Skip to content

Commit e4932db

Browse files
committed
add import/export data from file
1 parent f81adc1 commit e4932db

5 files changed

Lines changed: 247 additions & 3 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import adminService from '../services/adminService.js';
2+
import asyncHandler from '../utils/asyncHandler.js';
3+
4+
class AdminController {
5+
exportData = asyncHandler(async (req, res) => {
6+
const data = await adminService.exportAllData();
7+
res.status(200).json(data);
8+
});
9+
10+
importData = asyncHandler(async (req, res) => {
11+
const result = await adminService.importAllData(req.body);
12+
res.status(200).json(result);
13+
});
14+
}
15+
16+
export default new AdminController();

backend/src/routes/adminRoutes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import userRoutes from './userRoutes.js';
99
import equipmentRoutes from './equipmentRoutes.js';
1010
import roomAdminRoutes from './roomAdminRoutes.js';
1111
import logController from '../controllers/logController.js';
12+
import adminController from '../controllers/adminController.js';
1213

1314
const router = Router();
1415

@@ -20,6 +21,9 @@ router.use('/rooms', roomAdminRoutes);
2021

2122
router.get('/logs', logController.getBookingLogs);
2223

24+
router.get('/export-all', adminController.exportData);
25+
router.post('/import-all', adminController.importData);
26+
2327

2428
export default router;
2529

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import db from '../config/db.js';
2+
3+
class AdminService {
4+
collections = ['Users', 'Rooms', 'Computers', 'Bookings'];
5+
6+
// Обязательные поля для коллекций
7+
schema = {
8+
Users: ['email', 'full_name', 'is_admin', 'password', 'meta'],
9+
Rooms: ['name', 'grid', 'description', 'tags'],
10+
Computers: ['room_id', 'status', 'seat_index', 'inv_number', 'mac_address', 'status', 'software', 'specs', 'meta'],
11+
Bookings: ['_from', '_to', 'start_at', 'end_at', 'status', 'meta', 'history']
12+
};
13+
14+
validateData(data) {
15+
if (!data || typeof data !== 'object') throw new Error("Некорректный формат JSON");
16+
17+
for (const colName of this.collections) {
18+
const items = data[colName];
19+
if (!items) continue;
20+
if (!Array.isArray(items)) throw new Error(`Поле ${colName} должно быть массивом`);
21+
22+
const requiredFields = this.schema[colName];
23+
for (const [index, item] of items.entries()) {
24+
const missing = requiredFields.filter(field => !(field in item));
25+
if (missing.length > 0) {
26+
throw new Error(`Ошибка в коллекции ${colName} (объект #${index + 1}): отсутствуют поля [${missing.join(', ')}]`);
27+
}
28+
}
29+
}
30+
return true;
31+
}
32+
33+
async importAllData(data) {
34+
this.validateData(data);
35+
36+
for (const colName of this.collections) {
37+
if (!data[colName]) continue;
38+
39+
const collection = db.collection(colName);
40+
if (await collection.exists()) {
41+
await collection.truncate();
42+
await collection.import(data[colName]);
43+
}
44+
}
45+
return { message: "Данные успешно импортированы" };
46+
}
47+
48+
async exportAllData() {
49+
const fullDump = {};
50+
for (const colName of this.collections) {
51+
const collection = db.collection(colName);
52+
if (await collection.exists()) {
53+
const cursor = await db.query(`FOR d IN @@col RETURN d`, { '@col': colName });
54+
fullDump[colName] = await cursor.all();
55+
}
56+
}
57+
return fullDump;
58+
}
59+
}
60+
61+
export default new AdminService();

frontend/src/assets/scss/pages/_admin-dashboard.scss

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,60 @@
188188
font-size: 1.1rem;
189189
font-weight: 600;
190190
}
191+
192+
.mass-operations-section {
193+
margin-bottom: 24px;
194+
195+
.ops-card {
196+
background: #1a1a1a;
197+
border: 1px solid #333;
198+
border-radius: 12px;
199+
padding: 24px;
200+
display: flex;
201+
flex-direction: column;
202+
gap: 20px;
203+
204+
.ops-info {
205+
h3 { margin: 0 0 8px 0; font-size: 1.2rem; color: #fff; }
206+
p { margin: 0; color: #888; font-size: 0.9rem; line-height: 1.4; }
207+
}
208+
209+
.ops-actions {
210+
display: grid;
211+
grid-template-columns: 1fr 1fr;
212+
gap: 16px;
213+
214+
.import-zone {
215+
border: 2px dashed #333;
216+
border-radius: 8px;
217+
padding: 20px;
218+
text-align: center;
219+
cursor: pointer;
220+
transition: all 0.3s ease;
221+
&:hover { border-color: #3b82f6; background: rgba(59, 130, 246, 0.05); }
222+
223+
.import-placeholder {
224+
display: flex;
225+
flex-direction: column;
226+
align-items: center;
227+
gap: 8px;
228+
color: #aaa;
229+
.icon { font-size: 1.5rem; }
230+
}
231+
}
232+
233+
.btn-export-all {
234+
width: 100%;
235+
height: 100%;
236+
background: #2a2a2a;
237+
border: 1px solid #444;
238+
color: #fff;
239+
border-radius: 8px;
240+
font-weight: 600;
241+
cursor: pointer;
242+
transition: background 0.3s;
243+
&:hover { background: #333; }
244+
}
245+
}
246+
}
247+
}

frontend/src/views/AdminDashboard.vue

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,37 @@
1919
</div>
2020

2121
<div v-if="currentTab === 'summary'" class="tab-content">
22+
23+
<div class="mass-operations-section">
24+
<div class="ops-card">
25+
<div class="ops-info">
26+
<h3>Массовые операции с данными</h3>
27+
<p>Загрузите JSON-файл для восстановления базы или экспортируйте текущее состояние всех сущностей (Пользователи, Комнаты, ПК, Бронирования).</p>
28+
</div>
29+
30+
<div class="ops-actions">
31+
<div class="import-zone" @click="$refs.fileInput.click()">
32+
<input
33+
type="file"
34+
ref="fileInput"
35+
style="display: none"
36+
accept=".json"
37+
@change="handleFileImport"
38+
/>
39+
<div class="import-placeholder">
40+
<span>Нажмите, чтобы выбрать <strong>.json</strong></span>
41+
</div>
42+
</div>
43+
44+
<div class="export-zone">
45+
<button class="btn-export-all" @click="handleExport">
46+
Экспортировать всё в JSON
47+
</button>
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
2253
<div class="kpi-grid">
2354
<div class="kpi-card" v-for="stat in kpiStats" :key="stat.label">
2455
<span class="kpi-label">{{ stat.label }}</span>
@@ -62,10 +93,12 @@
6293

6394
<script setup>
6495
import { ref, onMounted } from 'vue'
96+
import { useAuthStore } from '@/stores/auth'
6597
import AdminLogs from './AdminLogs.vue'
6698
99+
const authStore = useAuthStore()
67100
const currentTab = ref('summary')
68-
const tabNames = { summary: 'Сводка', logs: 'Журнал', schedule: 'Расписание' }
101+
const tabNames = { summary: 'Сводка', logs: 'Журнал'}
69102
70103
const kpiStats = ref([
71104
{ label: 'В сети', value: '342', color: '#10B981' },
@@ -74,11 +107,84 @@ const kpiStats = ref([
74107
{ label: 'Неявки', value: '5.2%', color: '#F59E0B' }
75108
])
76109
110+
const handleExport = async () => {
111+
try {
112+
const response = await fetch('http://localhost:3000/api/admin/export-all', {
113+
headers: { 'Authorization': `Bearer ${authStore.token}` }
114+
});
115+
116+
if (!response.ok) throw new Error('Ошибка сервера');
117+
118+
const data = await response.json();
119+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
120+
const url = window.URL.createObjectURL(blob);
121+
122+
const link = document.createElement('a');
123+
link.href = url;
124+
link.setAttribute('download', `backup_${new Date().toLocaleDateString()}.json`);
125+
document.body.appendChild(link);
126+
link.click();
127+
link.remove();
128+
} catch (err) {
129+
alert('Не удалось экспортировать данные: ' + err.message);
130+
}
131+
}
132+
133+
const handleFileImport = (event) => {
134+
const file = event.target.files[0];
135+
if (!file) return;
136+
137+
const reader = new FileReader();
138+
reader.onload = async (e) => {
139+
try {
140+
let jsonData;
141+
142+
try {
143+
jsonData = JSON.parse(e.target.result);
144+
} catch (parseErr) {
145+
throw new Error("Файл не является корректным JSON-документом или поврежден.");
146+
}
147+
148+
const hasAnyCollection = ['Users', 'Rooms', 'Computers', 'Bookings'].some(key => key in jsonData);
149+
if (!hasAnyCollection) {
150+
throw new Error("В файле не найдено ни одной известной коллекции данных.");
151+
}
152+
153+
if (!confirm("ВНИМАНИЕ: Это действие полностью перезапишет базу данных. Вы уверены?")) {
154+
event.target.value = '';
155+
return;
156+
}
157+
158+
const response = await fetch('http://localhost:3000/api/admin/import-all', {
159+
method: 'POST',
160+
headers: {
161+
'Content-Type': 'application/json',
162+
'Authorization': `Bearer ${authStore.token}`
163+
},
164+
body: JSON.stringify(jsonData)
165+
});
166+
167+
const result = await response.json();
168+
169+
if (response.ok) {
170+
alert("Успех: " + result.message);
171+
window.location.reload();
172+
} else {
173+
throw new Error(result.message || 'Ошибка при импорте');
174+
}
175+
} catch (err) {
176+
alert("Ошибка валидации: " + err.message);
177+
} finally {
178+
event.target.value = '';
179+
}
180+
};
181+
reader.readAsText(file);
182+
}
183+
77184
onMounted(async () => {
78-
// TODO: запрос к бекенду за реальными KPI
79185
})
80186
</script>
81187

82188
<style lang="scss" scoped>
83189
@use "@/assets/scss/pages/admin-dashboard";
84-
</style>
190+
</style>

0 commit comments

Comments
 (0)