Files
mkd/components/admin/DataImportSection.tsx
2026-02-04 00:17:04 +05:00

145 lines
5.7 KiB
TypeScript
Executable File
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.
import React, { useState, useRef } from 'react';
import { backendApi } from '../../services/apiClient';
import { Upload, Download, Loader2, FileSpreadsheet } from 'lucide-react';
type ImportType = 'districts' | 'buildings' | 'employees' | 'accounts';
const IMPORT_LABELS: Record<ImportType, string> = {
districts: 'Участки',
buildings: 'Дома',
employees: 'Сотрудники',
accounts: 'Лицевые счета',
};
export const DataImportSection: React.FC = () => {
const [importType, setImportType] = useState<ImportType>('districts');
const [file, setFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{ created: number; errors: Array<{ row: number; message: string }>; total: number } | null>(null);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleDownloadTemplate = () => {
const url = backendApi.getImportTemplateUrl(importType);
window.open(url, '_blank');
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0];
setFile(f || null);
setResult(null);
setError(null);
};
const handleImport = async () => {
if (!file) {
setError('Выберите файл');
return;
}
setLoading(true);
setError(null);
setResult(null);
try {
const data = await backendApi.importData(importType, file);
setResult(data);
} catch (e: any) {
setError(e?.message || 'Ошибка импорта');
} finally {
setLoading(false);
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
return (
<div className="space-y-6">
<h3 className="text-lg font-bold text-slate-800">Загрузка данных</h3>
<p className="text-sm text-slate-500">
Скачайте шаблон, заполните его и загрузите файл (CSV или XLSX). Поддерживаются: участки, дома, сотрудники, лицевые счета.
</p>
<div className="rounded-xl border border-slate-200 bg-slate-50 p-6 space-y-4">
<div>
<label className="block text-xs font-bold text-slate-700 mb-2">Тип данных</label>
<select
value={importType}
onChange={(e) => {
setImportType(e.target.value as ImportType);
setFile(null);
setResult(null);
setError(null);
}}
className="w-full max-w-xs px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
{(Object.keys(IMPORT_LABELS) as ImportType[]).map((t) => (
<option key={t} value={t}>{IMPORT_LABELS[t]}</option>
))}
</select>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={handleDownloadTemplate}
className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50"
>
<Download className="w-4 h-4" /> Скачать шаблон
</button>
<label className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50 cursor-pointer">
<FileSpreadsheet className="w-4 h-4" />
Выбрать файл
<input
ref={fileInputRef}
type="file"
accept=".csv,.xlsx,.xls"
onChange={handleFileChange}
className="hidden"
/>
</label>
{file && (
<span className="flex items-center gap-2 px-3 py-2 text-slate-600 text-sm">
{file.name}
</span>
)}
<button
type="button"
onClick={handleImport}
disabled={loading || !file}
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white text-sm font-bold rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
Загрузить
</button>
</div>
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-700 text-sm">{error}</div>
)}
{result && (
<div className="rounded-lg border border-slate-200 bg-white p-4 text-sm">
<p className="font-medium text-slate-800">
Импорт завершён: создано/обновлено записей <strong>{result.created}</strong>, всего строк {result.total}.
</p>
{result.errors.length > 0 && (
<div className="mt-3">
<p className="text-slate-600 font-medium mb-1">Ошибки по строкам:</p>
<ul className="list-disc list-inside text-slate-600 space-y-0.5 max-h-40 overflow-y-auto">
{result.errors.slice(0, 20).map((e, i) => (
<li key={i}>Строка {e.row}: {e.message}</li>
))}
{result.errors.length > 20 && (
<li className="text-slate-500"> и ещё {result.errors.length - 20}</li>
)}
</ul>
</div>
)}
</div>
)}
</div>
</div>
);
};