обновил дизайн, добавил дашборд
This commit is contained in:
@@ -444,8 +444,12 @@ body {
|
||||
}
|
||||
|
||||
.select-layer-active {
|
||||
position: relative;
|
||||
z-index: 300;
|
||||
position: relative !important;
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
.custom-select-wrapper.open {
|
||||
z-index: 10002 !important;
|
||||
}
|
||||
|
||||
.custom-select-trigger {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="../css/components.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
</head>
|
||||
@@ -56,6 +57,24 @@
|
||||
</svg>
|
||||
<span>Временные слоты</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-tab="edu-forms">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
||||
<line x1="9" y1="7" x2="17" y2="7" />
|
||||
<line x1="9" y1="11" x2="15" y2="11" />
|
||||
</svg>
|
||||
<span>Формы обучения</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-tab="database">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||||
</svg>
|
||||
<span>База данных</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a href="/admin/" class="btn-back">
|
||||
|
||||
@@ -26,11 +26,27 @@ const ROUTES = {
|
||||
await module.initTimeSlotsSettings();
|
||||
}
|
||||
},
|
||||
database: {
|
||||
title: 'База данных',
|
||||
file: 'views/database.html',
|
||||
init: async () => {
|
||||
const module = await import('./views/database.js');
|
||||
await module.initDatabase();
|
||||
}
|
||||
},
|
||||
'edu-forms': {
|
||||
title: 'Формы обучения',
|
||||
file: 'views/edu-forms.html',
|
||||
init: async () => {
|
||||
const module = await import('./views/edu-forms.js');
|
||||
await module.initEduForms();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_TABS = {
|
||||
ADMIN: ['general', 'time-slots'],
|
||||
EDUCATION_OFFICE: ['time-slots']
|
||||
ADMIN: ['general', 'time-slots', 'database', 'edu-forms'],
|
||||
EDUCATION_OFFICE: ['time-slots', 'edu-forms']
|
||||
};
|
||||
const allowedTabs = new Set(ROLE_TABS[role] || []);
|
||||
const defaultTab = role === 'EDUCATION_OFFICE' ? 'time-slots' : 'general';
|
||||
|
||||
157
frontend/admin/settings/js/views/database.js
Normal file
157
frontend/admin/settings/js/views/database.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { api } from '../../../js/api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../../../js/utils.js';
|
||||
|
||||
export async function initDatabase() {
|
||||
const tenantsTbody = document.getElementById('tenants-tbody');
|
||||
const addTenantForm = document.getElementById('add-tenant-form');
|
||||
const statusInfo = document.getElementById('db-status-info');
|
||||
const btnTest = document.getElementById('btn-test-connection');
|
||||
|
||||
// === Загрузка статуса текущего подключения ===
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const data = await api.get('/api/database/status');
|
||||
const statusBadge = data.connected
|
||||
? '<span class="badge badge-available">Online</span>'
|
||||
: '<span class="badge badge-unavailable">Offline</span>';
|
||||
|
||||
statusInfo.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">Тенант:</span>
|
||||
<strong>${escapeHtml(data.tenant || '—')}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">Название:</span>
|
||||
<strong>${escapeHtml(data.name || '—')}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">Статус:</span>
|
||||
${statusBadge}
|
||||
</div>
|
||||
${data.url ? `<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">URL:</span>
|
||||
<code style="font-size: 0.85rem;">${escapeHtml(data.url)}</code>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} catch (e) {
|
||||
statusInfo.innerHTML = `<div class="form-alert error" style="display:block">Ошибка загрузки статуса: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// === Загрузка списка тенантов ===
|
||||
async function loadTenants() {
|
||||
try {
|
||||
const tenants = await api.get('/api/database/tenants');
|
||||
renderTenantsTable(tenants);
|
||||
} catch (e) {
|
||||
tenantsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTenantsTable(tenants) {
|
||||
if (!tenants || !tenants.length) {
|
||||
tenantsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Нет подключённых тенантов</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tenantsTbody.innerHTML = tenants.map(t => {
|
||||
const statusBadge = t.connected
|
||||
? '<span class="badge badge-available">Online</span>'
|
||||
: '<span class="badge badge-unavailable">Offline</span>';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${escapeHtml(t.name || '—')}</td>
|
||||
<td><code>${escapeHtml(t.domain)}</code></td>
|
||||
<td><code style="font-size: 0.82rem;">${escapeHtml(t.url)}</code></td>
|
||||
<td>${escapeHtml(t.username || '—')}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td><button class="btn-delete" data-domain="${escapeHtml(t.domain)}">Удалить</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// === Тест подключения ===
|
||||
btnTest.addEventListener('click', async () => {
|
||||
hideAlert('add-tenant-alert');
|
||||
const url = document.getElementById('tenant-url').value.trim();
|
||||
const username = document.getElementById('tenant-username').value.trim();
|
||||
const password = document.getElementById('tenant-password').value;
|
||||
|
||||
if (!url) {
|
||||
showAlert('add-tenant-alert', 'Введите JDBC URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
btnTest.textContent = '...';
|
||||
btnTest.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await api.post('/api/database/test', { url, username, password });
|
||||
if (result.success) {
|
||||
showAlert('add-tenant-alert', '✓ Подключение успешно!', 'success');
|
||||
} else {
|
||||
showAlert('add-tenant-alert', `✗ ${result.message}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('add-tenant-alert', `Ошибка: ${e.message}`, 'error');
|
||||
} finally {
|
||||
btnTest.textContent = 'Тест';
|
||||
btnTest.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// === Добавление тенанта ===
|
||||
addTenantForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('add-tenant-alert');
|
||||
|
||||
const name = document.getElementById('tenant-name').value.trim();
|
||||
const domain = document.getElementById('tenant-domain').value.trim().toLowerCase();
|
||||
const url = document.getElementById('tenant-url').value.trim();
|
||||
const username = document.getElementById('tenant-username').value.trim();
|
||||
const password = document.getElementById('tenant-password').value;
|
||||
|
||||
if (!name || !domain || !url) {
|
||||
showAlert('add-tenant-alert', 'Заполните все обязательные поля', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.post('/api/database/tenants', { name, domain, url, username, password });
|
||||
if (result.success) {
|
||||
showAlert('add-tenant-alert', `Тенант "${escapeHtml(domain)}" добавлен!`, 'success');
|
||||
addTenantForm.reset();
|
||||
loadTenants();
|
||||
loadStatus();
|
||||
} else {
|
||||
showAlert('add-tenant-alert', result.message, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('add-tenant-alert', `Ошибка: ${e.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// === Удаление тенанта ===
|
||||
tenantsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
|
||||
const domain = btn.dataset.domain;
|
||||
if (!confirm(`Удалить тенант "${domain}"? Пул соединений будет закрыт.`)) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/api/database/tenants/${domain}`);
|
||||
loadTenants();
|
||||
loadStatus();
|
||||
} catch (e) {
|
||||
alert(`Ошибка: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// === Init ===
|
||||
loadStatus();
|
||||
loadTenants();
|
||||
}
|
||||
71
frontend/admin/settings/js/views/edu-forms.js
Normal file
71
frontend/admin/settings/js/views/edu-forms.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { api } from '../../../js/api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../../../js/utils.js';
|
||||
|
||||
export let allEducationForms = [];
|
||||
|
||||
export async function fetchEducationForms() {
|
||||
try {
|
||||
allEducationForms = await api.get('/api/education-forms');
|
||||
return allEducationForms;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch education forms", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function initEduForms() {
|
||||
const efTbody = document.getElementById('ef-tbody');
|
||||
const createEfForm = document.getElementById('create-ef-form');
|
||||
|
||||
async function loadEF() {
|
||||
try {
|
||||
const forms = await fetchEducationForms();
|
||||
renderEfTable(forms);
|
||||
} catch (e) {
|
||||
efTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderEfTable(forms) {
|
||||
if (!forms || !forms.length) {
|
||||
efTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет форм обучения</td></tr>';
|
||||
return;
|
||||
}
|
||||
efTbody.innerHTML = forms.map(ef => `
|
||||
<tr>
|
||||
<td>${ef.id}</td>
|
||||
<td>${escapeHtml(ef.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${ef.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createEfForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-ef-alert');
|
||||
const name = document.getElementById('new-ef-name').value.trim();
|
||||
if (!name) { showAlert('create-ef-alert', 'Введите название', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/education-forms', { name });
|
||||
showAlert('create-ef-alert', `Форма "${escapeHtml(data.name)}" создана`, 'success');
|
||||
createEfForm.reset();
|
||||
loadEF();
|
||||
} catch (e) {
|
||||
showAlert('create-ef-alert', e.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
efTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить форму обучения?')) return;
|
||||
try {
|
||||
await api.delete('/api/education-forms/' + btn.dataset.id);
|
||||
loadEF();
|
||||
} catch (e) {
|
||||
alert(e.message || 'Ошибка удаления');
|
||||
}
|
||||
});
|
||||
|
||||
loadEF();
|
||||
}
|
||||
73
frontend/admin/settings/views/database.html
Normal file
73
frontend/admin/settings/views/database.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<!-- ===== Database / Tenants Tab ===== -->
|
||||
|
||||
<!-- Текущее подключение -->
|
||||
<div class="card">
|
||||
<h2>Текущее подключение</h2>
|
||||
<div id="db-status-info" class="db-status-card">
|
||||
<div class="loading-row">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Таблица тенантов -->
|
||||
<div class="card">
|
||||
<h2>Подключённые университеты</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="tenants-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Домен</th>
|
||||
<th>JDBC URL</th>
|
||||
<th>Пользователь</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tenants-tbody">
|
||||
<tr>
|
||||
<td colspan="6" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Добавить тенант -->
|
||||
<div class="card create-card">
|
||||
<h2>Добавить подключение</h2>
|
||||
<form id="add-tenant-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="tenant-name">Название университета</label>
|
||||
<input type="text" id="tenant-name" placeholder="ЮЗГУ" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tenant-domain">Поддомен</label>
|
||||
<input type="text" id="tenant-domain" placeholder="swsu" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 0.75rem;">
|
||||
<div class="form-group" style="flex: 3;">
|
||||
<label for="tenant-url">JDBC URL</label>
|
||||
<input type="text" id="tenant-url" placeholder="jdbc:postgresql://192.168.1.50:5432/magistr_db" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 0.75rem;">
|
||||
<div class="form-group">
|
||||
<label for="tenant-username">Пользователь</label>
|
||||
<input type="text" id="tenant-username" placeholder="postgres" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tenant-password">Пароль</label>
|
||||
<input type="password" id="tenant-password" placeholder="••••••••" required>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" id="btn-test-connection" style="height: fit-content;">
|
||||
Тест
|
||||
</button>
|
||||
<button type="submit" class="btn-primary" style="height: fit-content;">
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-alert" id="add-tenant-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
34
frontend/admin/settings/views/edu-forms.html
Normal file
34
frontend/admin/settings/views/edu-forms.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!-- ===== Education Forms Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новая форма обучения</h2>
|
||||
<form id="create-ef-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-ef-name">Название</label>
|
||||
<input type="text" id="new-ef-name" placeholder="Бакалавриат" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-ef-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Все формы обучения</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="ef-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ef-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user