Initial commit: ExaMetra v1.0.0 — Free & open-source competence & grading platform

Features:
- 15+ views: Dashboard, Classes, Progress, Flower, Matrix, Assessments, Grading, Reports, Attendance, Calendar, Lesson Plans, Analytics, Parent Communication, Behavior Tracking, Settings
- 34+ API endpoints
- 30+ Prisma models
- 700+ i18n keys (German + English)
- Keyboard shortcuts (Cmd+K, Cmd+1-9, Cmd+/)
- Batch deletion, drag-and-drop seating, custom color palette
- Demo accounts (admin, teacher, student, parent)
- Dark mode, responsive design, print styles
- CSV/PDF export, notification system, onboarding tour
- Zero emojis — all replaced with Lucide icons
- MIT License
This commit is contained in:
Mustafa Cetin committed 2026-07-29 05:46:10 +00:00
commit 0c1f739989
156 files changed
+51090

No files matched your search

+280
View File
@@ -0,0 +1,280 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
function escapeCsvField(field: string | number | null | undefined): string {
if (field === null || field === undefined) return '';
const str = String(field);
// Escape fields containing quotes, commas, or newlines
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
}
function toCsvRow(fields: (string | number | null | undefined)[]): string {
return fields.map(escapeCsvField).join(',');
}
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 type = searchParams.get('type') || 'students';
const classGroupId = searchParams.get('classGroupId');
const schoolYearId = searchParams.get('schoolYearId');
const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined;
let csvContent = '';
let filename = `${type}_export.csv`;
if (type === 'students') {
// Students CSV
const where: Record<string, unknown> = { deletedAt: null };
if (schoolId) where.schoolId = schoolId;
let students = await db.student.findMany({
where,
orderBy: { lastName: 'asc' },
include: {
school: { select: { name: true } },
enrollments: {
where: classGroupId ? { classGroupId } : {},
include: {
classGroup: { select: { name: true, gradeLevel: true } },
},
},
_count: {
select: { learningProgressEntries: true, assessmentResults: true },
},
},
});
// Filter by school year if specified
if (schoolYearId && !classGroupId) {
students = students.filter((s) =>
s.enrollments.some((e) => e.schoolYearId === schoolYearId)
);
} else if (classGroupId) {
students = students.filter((s) =>
s.enrollments.some((e) => e.classGroupId === classGroupId)
);
}
const header = ['ID', 'First Name', 'Last Name', 'Date of Birth', 'External ID', 'School', 'Class', 'Grade Level', 'Progress Entries', 'Assessment Results'];
csvContent = header.map(escapeCsvField).join(',') + '\n';
for (const s of students) {
const enrollment = s.enrollments[0];
csvContent += toCsvRow([
s.id,
s.firstName,
s.lastName,
s.dateOfBirth ? new Date(s.dateOfBirth).toLocaleDateString() : '',
s.externalId ?? '',
s.school.name,
enrollment?.classGroup?.name ?? '',
enrollment?.classGroup?.gradeLevel ?? '',
s._count.learningProgressEntries,
s._count.assessmentResults,
]) + '\n';
}
filename = `students_export.csv`;
} else if (type === 'progress') {
// Progress entries CSV
const where: Record<string, unknown> = {};
if (classGroupId) where.classGroupId = classGroupId;
let entries = await db.learningProgressEntry.findMany({
where,
orderBy: { date: 'desc' },
include: {
student: { select: { id: true, firstName: true, lastName: true } },
competency: {
select: {
id: true,
code: true,
title: true,
category: { select: { id: true, name: true } },
},
},
teacher: { select: { id: true, firstName: true, lastName: true } },
classGroup: { select: { id: true, name: true } },
},
});
// Filter by school year through classGroup
if (schoolYearId) {
const classGroups = await db.classGroup.findMany({
where: { schoolYearId },
select: { id: true },
});
const classIds = classGroups.map((c) => c.id);
entries = entries.filter((e) => classIds.includes(e.classGroupId));
}
// Filter by school
if (schoolId) {
const classGroups = await db.classGroup.findMany({
where: { schoolId },
select: { id: true },
});
const classIds = classGroups.map((c) => c.id);
entries = entries.filter((e) => classIds.includes(e.classGroupId));
}
const header = ['ID', 'Date', 'Student First Name', 'Student Last Name', 'Student ID', 'Competency Code', 'Competency Title', 'Category', 'Mastery Level', 'Note', 'Teacher', 'Class'];
csvContent = header.map(escapeCsvField).join(',') + '\n';
for (const e of entries) {
csvContent += toCsvRow([
e.id,
new Date(e.date).toLocaleDateString(),
e.student.firstName,
e.student.lastName,
e.student.id,
e.competency.code,
e.competency.title,
e.competency.category.name,
e.masteryLevelValue,
e.note ?? '',
`${e.teacher.firstName} ${e.teacher.lastName}`,
e.classGroup.name,
]) + '\n';
}
filename = `progress_export.csv`;
} else if (type === 'assessments') {
// Assessments CSV
const where: Record<string, unknown> = {};
if (classGroupId) where.classGroupId = classGroupId;
let assessments = await db.assessment.findMany({
where,
orderBy: { date: 'desc' },
include: {
classGroup: { select: { id: true, name: true } },
subject: { select: { id: true, name: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
assessmentResults: {
include: {
student: { select: { id: true, firstName: true, lastName: true } },
},
},
},
});
if (schoolYearId) {
const classGroups = await db.classGroup.findMany({
where: { schoolYearId },
select: { id: true },
});
const classIds = classGroups.map((c) => c.id);
assessments = assessments.filter((a) => classIds.includes(a.classGroupId));
}
if (schoolId) {
const classGroups = await db.classGroup.findMany({
where: { schoolId },
select: { id: true },
});
const classIds = classGroups.map((c) => c.id);
assessments = assessments.filter((a) => classIds.includes(a.classGroupId));
}
const header = ['Assessment ID', 'Title', 'Date', 'Type', 'Subject', 'Class', 'Teacher', 'Max Score', 'Weight', 'Student ID', 'Student Name', 'Score', 'Mastery Level', 'Note'];
csvContent = header.map(escapeCsvField).join(',') + '\n';
for (const a of assessments) {
if (a.assessmentResults.length === 0) {
csvContent += toCsvRow([
a.id, a.title, new Date(a.date).toLocaleDateString(), a.type,
a.subject.name, a.classGroup.name,
`${a.teacher.firstName} ${a.teacher.lastName}`,
a.maxScore ?? '', a.weight, '', '', '', '', '',
]) + '\n';
} else {
for (const r of a.assessmentResults) {
csvContent += toCsvRow([
a.id, a.title, new Date(a.date).toLocaleDateString(), a.type,
a.subject.name, a.classGroup.name,
`${a.teacher.firstName} ${a.teacher.lastName}`,
a.maxScore ?? '', a.weight,
r.studentId, `${r.student.firstName} ${r.student.lastName}`,
r.score ?? '', r.masteryLevelValue ?? '', r.note ?? '',
]) + '\n';
}
}
}
filename = `assessments_export.csv`;
} else if (type === 'grades') {
// Grades CSV
const where: Record<string, unknown> = {};
if (classGroupId) where.classGroupId = classGroupId;
if (schoolYearId) where.schoolYearId = schoolYearId;
let grades = await db.computedGrade.findMany({
where,
orderBy: [{ student: { lastName: 'asc' } }],
include: {
student: { select: { id: true, firstName: true, lastName: true } },
subject: { select: { id: true, name: true } },
classGroup: { select: { id: true, name: true } },
schoolYear: { select: { id: true, label: true } },
},
});
if (schoolId && !classGroupId) {
const classGroups = await db.classGroup.findMany({
where: { schoolId },
select: { id: true },
});
const classIds = classGroups.map((c) => c.id);
grades = grades.filter((g) => classIds.includes(g.classGroupId));
}
const header = ['Student ID', 'Student First Name', 'Student Last Name', 'Subject', 'Class', 'School Year', 'Period', 'Computed Value', 'Overridden Value', 'Override Reason', 'Finalized'];
csvContent = header.map(escapeCsvField).join(',') + '\n';
for (const g of grades) {
csvContent += toCsvRow([
g.studentId, g.student.firstName, g.student.lastName,
g.subject.name, g.classGroup.name, g.schoolYear.label,
g.period, g.computedValue.toFixed(2),
g.overriddenValue?.toFixed(2) ?? '', g.overrideReason ?? '',
g.isFinalized ? 'Yes' : 'No',
]) + '\n';
}
filename = `grades_export.csv`;
}
// Create audit log for export
await db.auditLog.create({
data: {
userId: session.userId,
schoolId: session.user?.schoolId ?? null,
action: 'EXPORT',
entityType: 'DataExport',
entityId: null,
metadata: JSON.stringify({ type, classGroupId, schoolYearId }),
},
});
return new NextResponse(csvContent, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': `attachment; filename="${filename}"`,
},
});
} catch (error) {
console.error('CSV Export error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}