Files
lms-sb/src/components/admin/csv-exporter.tsx
T
admins dd46a10c20 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>
2026-04-08 12:51:43 +05:00

92 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}