Initial commit
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { DownloadIcon, UploadIcon } from '../utils/svgs';
|
||||
|
||||
const ImportExport = ({ token, onRefreshData }) => {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [status, setStatus] = useState({ type: '', message: '' });
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const onButtonClick = () => {
|
||||
fileInputRef.current.click();
|
||||
};
|
||||
|
||||
const handleDrag = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === "dragenter" || e.type === "dragover") {
|
||||
setDragActive(true);
|
||||
} else if (e.type === "dragleave") {
|
||||
setDragActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
handleFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
handleFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFile = (file) => {
|
||||
const reader = new FileReader();
|
||||
const fileType = file.name.split('.').pop().toLowerCase();
|
||||
|
||||
reader.onload = async (e) => {
|
||||
const text = e.target.result;
|
||||
try {
|
||||
let payload = null;
|
||||
|
||||
if (fileType === 'json') {
|
||||
payload = JSON.parse(text);
|
||||
if (!payload.employees || !payload.departments || !payload.companies) {
|
||||
throw new Error('Некорректный формат JSON. Ожидались массивы employees, departments и companies.');
|
||||
}
|
||||
} else if (fileType === 'csv') {
|
||||
const rows = text.split('\n').map(row => row.trim()).filter(Boolean);
|
||||
if (rows.length < 2) {
|
||||
throw new Error('CSV файл пуст или содержит недостаточно строк.');
|
||||
}
|
||||
|
||||
// Expected Headers: ФИО;Компания;Отдел;Должность;Телефон;Email;Здание;Этаж
|
||||
// Parse semicolon separated columns
|
||||
const employees = [];
|
||||
const departmentsSet = new Set();
|
||||
const companiesSet = new Set();
|
||||
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
// Split by semicolon and clean quotes
|
||||
const values = rows[i].split(';').map(v => v.trim().replace(/^"|"$/g, ''));
|
||||
|
||||
const emp = {
|
||||
name: values[0] || '',
|
||||
company_name: values[1] || '',
|
||||
department_name: values[2] || '',
|
||||
job_title: values[3] || '',
|
||||
phone: values[4] || '',
|
||||
email: values[5] || '',
|
||||
building: values[6] || '',
|
||||
floor: values[7] || ''
|
||||
};
|
||||
|
||||
if (emp.name && emp.job_title) {
|
||||
employees.push(emp);
|
||||
if (emp.department_name) {
|
||||
departmentsSet.add(emp.department_name);
|
||||
}
|
||||
if (emp.company_name) {
|
||||
companiesSet.add(emp.company_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const departments = Array.from(departmentsSet).map((name, index) => ({
|
||||
id: index + 1,
|
||||
name
|
||||
}));
|
||||
|
||||
const companies = Array.from(companiesSet).map((name, index) => ({
|
||||
id: index + 1,
|
||||
name
|
||||
}));
|
||||
|
||||
const mappedEmployees = employees.map(emp => {
|
||||
const dept = departments.find(d => d.name === emp.department_name);
|
||||
const comp = companies.find(c => c.name === emp.company_name);
|
||||
return {
|
||||
name: emp.name,
|
||||
job_title: emp.job_title,
|
||||
phone: emp.phone,
|
||||
email: emp.email,
|
||||
building: emp.building,
|
||||
floor: emp.floor,
|
||||
department_id: dept ? dept.id : null,
|
||||
company_id: comp ? comp.id : null
|
||||
};
|
||||
});
|
||||
|
||||
payload = { employees: mappedEmployees, departments, companies };
|
||||
} else {
|
||||
throw new Error('Поддерживаются только файлы .json и .csv');
|
||||
}
|
||||
|
||||
setStatus({ type: 'info', message: 'Импортирование данных...' });
|
||||
const res = await fetch('/api/admin/import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setStatus({ type: 'success', message: `Импорт завершен! Загружено компаний: ${payload.companies.length}, отделов: ${payload.departments.length}, сотрудников: ${payload.employees.length}` });
|
||||
onRefreshData();
|
||||
} else {
|
||||
setStatus({ type: 'error', message: data.error || 'Ошибка при импорте на сервер.' });
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus({ type: 'error', message: `Ошибка чтения файла: ${err.message}` });
|
||||
}
|
||||
};
|
||||
|
||||
// Explicitly read file in UTF-8
|
||||
reader.readAsText(file, "UTF-8");
|
||||
};
|
||||
|
||||
const handleExportJSON = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/export', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) throw new Error('Ошибка экспорта данных с сервера');
|
||||
const data = await res.json();
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `addressbook_backup_${new Date().toISOString().slice(0, 10)}.json`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
setStatus({ type: 'error', message: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportCSV = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/export', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) throw new Error('Ошибка экспорта данных с сервера');
|
||||
const data = await res.json();
|
||||
|
||||
const headers = ['ФИО', 'Компания', 'Отдел', 'Должность', 'Телефон', 'Email', 'Здание', 'Этаж'];
|
||||
// Semicolon separator
|
||||
const csvRows = [headers.join(';')];
|
||||
|
||||
for (const emp of data.employees) {
|
||||
const dept = data.departments.find(d => d.id === emp.department_id);
|
||||
const comp = data.companies.find(c => c.id === emp.company_id);
|
||||
const row = [
|
||||
`"${emp.name.replace(/"/g, '""')}"`,
|
||||
`"${(comp ? comp.name : '').replace(/"/g, '""')}"`,
|
||||
`"${(dept ? dept.name : '').replace(/"/g, '""')}"`,
|
||||
`"${(emp.job_title || '').replace(/"/g, '""')}"`,
|
||||
`"${(emp.phone || '').replace(/"/g, '""')}"`,
|
||||
`"${(emp.email || '').replace(/"/g, '""')}"`,
|
||||
`"${(emp.building || '').replace(/"/g, '""')}"`,
|
||||
`"${(emp.floor || '').replace(/"/g, '""')}"`
|
||||
];
|
||||
csvRows.push(row.join(';'));
|
||||
}
|
||||
|
||||
const blob = new Blob(['\uFEFF' + csvRows.join('\r\n')], { type: 'text/csv;charset=utf-8;' }); // Excel BOM
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `addressbook_roster_${new Date().toISOString().slice(0, 10)}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
setStatus({ type: 'error', message: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="import-export-section">
|
||||
<div className="admin-card">
|
||||
<h3 style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<UploadIcon style={{ width: '1.25rem', height: '1.25rem' }} /> Импорт данных (Бэкап / CSV)
|
||||
</h3>
|
||||
|
||||
{status.message && (
|
||||
<div className={`alert-banner alert-${status.type}`}>
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form id="form-file-upload" onDragEnter={handleDrag} onSubmit={(e) => e.preventDefault()}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
id="input-file-upload"
|
||||
multiple={false}
|
||||
accept=".json,.csv"
|
||||
onChange={handleFileInputChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<div
|
||||
className={`dropzone ${dragActive ? "drag-active" : ""}`}
|
||||
onClick={onButtonClick}
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<p>Перетащите сюда файл резервной копии <strong>.json</strong> или список сотрудников <strong>.csv</strong></p>
|
||||
<button className="btn btn-secondary" style={{ marginTop: '1.1rem' }} type="button">
|
||||
Выбрать файл на компьютере
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.75rem' }}>
|
||||
* При импорте текущая база будет очищена и перезаписана! Структура колонок CSV (разделитель - <strong>точка с запятой ";"</strong>): ФИО;Компания;Отдел;Должность;Телефон;Email;Здание;Этаж. Кодировка UTF-8.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-card" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<DownloadIcon style={{ width: '1.25rem', height: '1.25rem' }} /> Экспорт данных (Бэкап)
|
||||
</h3>
|
||||
<p style={{ color: 'var(--text-muted)', marginBottom: '1.5rem', fontSize: '0.875rem' }}>
|
||||
Экспортируйте базу данных в любой момент. Скачайте структурированный бэкап в формате JSON для восстановления на другом компьютере, или скачайте плоский список сотрудников в формате CSV для редактирования в Excel.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-primary" onClick={handleExportJSON} style={{ flex: '1' }}>
|
||||
<DownloadIcon style={{ width: '1.1rem', height: '1.1rem' }} /> Скачать JSON Бэкап
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={handleExportCSV} style={{ flex: '1' }}>
|
||||
<DownloadIcon style={{ width: '1.1rem', height: '1.1rem' }} /> Скачать CSV Таблицу
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportExport;
|
||||
Reference in New Issue
Block a user