Files
corp-address-book/frontend/src/components/ImportExport.jsx
T

292 lines
12 KiB
React
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 { DownloadIcon, UploadIcon } from '../utils/svgs';
const ImportExport = ({ token, onRefreshData, onLogout }) => {
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] || '',
internal_phone: values[8] || ''
};
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,
internal_phone: emp.internal_phone,
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)
});
if (res.status === 401 || res.status === 403) {
onLogout('Сессия истекла, войдите заново');
return;
}
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.status === 401 || res.status === 403) {
onLogout('Сессия истекла, войдите заново');
return;
}
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.status === 401 || res.status === 403) {
onLogout('Сессия истекла, войдите заново');
return;
}
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, '""')}"`,
`"${(emp.internal_phone || '').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;