Add CSV import/export for students (Stage 11)
Import wizard (4 steps): - Upload CSV with UTF-8 or Windows-1251 (iconv-lite) decoding - Auto-detect columns: Email, Имя, Фамилия, Телефон - Preview table with per-row status: new / update / error - Options: auto-verify email, assign course + access days, send welcome email - Apply: creates users with bcrypt password + Account record, grants enrollments Export: - GET /api/admin/export-users with course filter + encoding selection - UTF-8 with BOM (works in all apps) or Windows-1251 (legacy Excel) - Fields: Email, Имя, Телефон, Дата регистрации, Курсы, Прогресс Navigation: added "Импорт / Экспорт" link to admin sidebar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import iconv from "iconv-lite";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session || session.user.role !== "admin") {
|
||||
return NextResponse.json({ error: "Нет доступа" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { searchParams } = request.nextUrl;
|
||||
const courseId = searchParams.get("courseId") || undefined;
|
||||
const encoding = (searchParams.get("encoding") as "utf8" | "win1251") ?? "utf8";
|
||||
|
||||
// Fetch users
|
||||
const users = await prisma.user.findMany({
|
||||
where: courseId
|
||||
? { enrollments: { some: { courseId } } }
|
||||
: { role: "student" },
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: {
|
||||
enrollments: {
|
||||
include: { course: { select: { title: true } } },
|
||||
},
|
||||
progress: { select: { lessonId: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Build CSV rows
|
||||
const csvHeaders = ["Email", "Имя", "Телефон", "Дата регистрации", "Курсы", "Прогресс (уроков)"];
|
||||
const rows = users.map((u) => {
|
||||
const courses = u.enrollments.map((e) => e.course.title).join(" | ");
|
||||
const progress = u.progress.length;
|
||||
const registeredAt = new Date(u.createdAt).toLocaleDateString("ru-RU");
|
||||
return [u.email, u.name, "", registeredAt, courses, String(progress)];
|
||||
});
|
||||
|
||||
const allRows = [csvHeaders, ...rows];
|
||||
const csvText = allRows
|
||||
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(";"))
|
||||
.join("\r\n");
|
||||
|
||||
// Encode
|
||||
let body: Buffer;
|
||||
let charset: string;
|
||||
if (encoding === "win1251") {
|
||||
body = iconv.encode(csvText, "win1251");
|
||||
charset = "windows-1251";
|
||||
} else {
|
||||
body = Buffer.from("\uFEFF" + csvText, "utf8"); // BOM for Excel
|
||||
charset = "utf-8";
|
||||
}
|
||||
|
||||
const filename = `students_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||
|
||||
return new NextResponse(body as unknown as BodyInit, {
|
||||
headers: {
|
||||
"Content-Type": `text/csv; charset=${charset}`,
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user