/
aaronair
/
Test_task_rt_solution
Обзор
Документация
Войти
/
aaronair
/
Test_task_rt_solution
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/ImportModal.tsx
162 строки
5 KB
Ilikedrinkpivo
first_commit
28 май 2026, 14:51
28 май 2026, 14:51
b4498fc
Код
Авторство
О чём код?
import { useEffect, useId, useRef, useState } from "react"; import { importInitiatives } from "@/features/initiatives/api"; import { showToast } from "@/components/Toast"; interface Props { open: boolean; onClose: () => void; } const SAMPLE_COLUMNS = [ "Название", "Сегмент", "ФИО владельца", "E-mail", "Подразделение", "Исполнитель", "Статус", "Описание", "Эффект", "Бюджет", ]; export function ImportModal({ open, onClose }: Props) { const titleId = useId(); const closeRef = useRef<HTMLButtonElement>(null); const [dragOver, setDragOver] = useState(false); const [errors, setErrors] = useState<{ row: number; message: string }[]>([]); const [importing, setImporting] = useState(false); useEffect(() => { if (!open) return; closeRef.current?.focus(); setErrors([]); const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = prev; }; }, [open, onClose]); const handleFile = async (file: File | undefined) => { if (!file) return; setImporting(true); setErrors([]); try { const dry = await importInitiatives(file, true); if (dry.errors.length > 0) { setErrors(dry.errors); showToast(`Проверка: ${dry.errors.length} ошибок`); return; } const result = await importInitiatives(file, false); showToast(`Импортировано строк: ${result.imported}`); onClose(); } catch { showToast("Ошибка импорта файла"); } finally { setImporting(false); } }; if (!open) return null; return ( <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <button type="button" className="absolute inset-0 bg-ink-900/40 backdrop-blur-sm" aria-label="Закрыть" onClick={onClose} /> <div role="dialog" aria-modal="true" aria-labelledby={titleId} className="relative w-full max-w-lg rounded-xl2 bg-surface-card p-6 shadow-cardHover border border-ink-300/40" > <h2 id={titleId} className="text-xl font-bold text-ink-900"> Импорт из Excel </h2> <p className="mt-2 text-sm text-ink-600"> Загрузите файл .xlsx с инициативами. Сначала выполняется проверка, затем импорт в реестр. </p> <div className={`mt-4 rounded-lg border-2 border-dashed p-8 text-center transition-colors ${ dragOver ? "border-primary-400 bg-primary-50" : "border-ink-300/60 bg-surface-muted" }`} onDragOver={(e) => { e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); void handleFile(e.dataTransfer.files[0]); }} > <p className="text-sm text-ink-600"> {importing ? "Импорт…" : "Перетащите файл сюда или "} {!importing && ( <label className="text-primary-700 font-medium cursor-pointer hover:underline"> выберите на диске <input type="file" accept=".xlsx,.xls" className="sr-only" onChange={(e) => void handleFile(e.target.files?.[0])} /> </label> )} </p> </div> {errors.length > 0 && ( <ul className="mt-4 max-h-32 overflow-y-auto text-xs text-red-700 space-y-1"> {errors.map((err) => ( <li key={`${err.row}-${err.message}`}> Строка {err.row}: {err.message} </li> ))} </ul> )} <p className="mt-4 text-xs font-medium uppercase tracking-wide text-ink-500"> Ожидаемые столбцы </p> <div className="mt-2 overflow-x-auto rounded-lg border border-ink-300/40"> <table className="min-w-full text-xs"> <thead> <tr className="bg-surface-muted"> {SAMPLE_COLUMNS.map((c) => ( <th key={c} className="px-2 py-1.5 text-left font-medium text-ink-600"> {c} </th> ))} </tr> </thead> </table> </div> <div className="mt-6 flex justify-end gap-3"> <button type="button" className="btn-secondary" onClick={onClose}> Отмена </button> <button ref={closeRef} type="button" className="btn-primary" onClick={onClose}> Закрыть </button> </div> </div> </div> ); }