Initial commit

This commit is contained in:
fabritsky
2026-05-25 09:45:08 +00:00
commit 662842019c
34 changed files with 10033 additions and 0 deletions
+275
View File
@@ -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;