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,91 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Download } from "lucide-react";
|
||||
|
||||
type Course = { id: string; title: string };
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
border: "2px solid var(--border)",
|
||||
background: "var(--background)",
|
||||
outline: "none",
|
||||
width: "100%",
|
||||
padding: "0.5rem 0.75rem",
|
||||
fontSize: "0.875rem",
|
||||
fontFamily: "inherit",
|
||||
};
|
||||
const focusHandlers = {
|
||||
onFocus: (e: React.FocusEvent<HTMLSelectElement>) =>
|
||||
(e.currentTarget.style.borderColor = "var(--foreground)"),
|
||||
onBlur: (e: React.FocusEvent<HTMLSelectElement>) =>
|
||||
(e.currentTarget.style.borderColor = "var(--border)"),
|
||||
};
|
||||
|
||||
export function CsvExporter({ courses }: { courses: Course[] }) {
|
||||
const [courseId, setCourseId] = useState("");
|
||||
const [encoding, setEncoding] = useState<"utf8" | "win1251">("utf8");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleExport() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ encoding });
|
||||
if (courseId) params.set("courseId", courseId);
|
||||
|
||||
const res = await fetch(`/api/admin/export-users?${params}`);
|
||||
if (!res.ok) throw new Error("Ошибка сервера");
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
const cd = res.headers.get("content-disposition") ?? "";
|
||||
const match = cd.match(/filename="([^"]+)"/);
|
||||
a.download = match?.[1] ?? "students.csv";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5 max-w-md">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>
|
||||
Фильтр по курсу
|
||||
</label>
|
||||
<select value={courseId} onChange={(e) => setCourseId(e.target.value)} style={{ ...inputStyle, appearance: "none" }} {...focusHandlers}>
|
||||
<option value="">Все ученики</option>
|
||||
{courses.map((c) => <option key={c.id} value={c.id}>{c.title}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>
|
||||
Кодировка файла
|
||||
</label>
|
||||
<select value={encoding} onChange={(e) => setEncoding(e.target.value as "utf8" | "win1251")} style={{ ...inputStyle, appearance: "none" }} {...focusHandlers}>
|
||||
<option value="utf8">UTF-8 (универсальная)</option>
|
||||
<option value="win1251">Windows-1251 (для Excel на Windows)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-1" style={{ border: "2px solid var(--border)" }}>
|
||||
<p className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>Поля в файле</p>
|
||||
<p className="text-sm">Email · Имя · Телефон · Дата регистрации · Курсы · Прогресс (уроков)</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleExport}
|
||||
disabled={loading}
|
||||
className="btn-aubade btn-aubade-accent flex items-center gap-2 px-5 py-2 text-sm"
|
||||
style={{ opacity: loading ? 0.6 : 1 }}
|
||||
>
|
||||
<Download size={14} />
|
||||
{loading ? "Формирую файл..." : "Скачать CSV"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user