286 lines
11 KiB
HTML
286 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru" data-title="Панель управления" data-description="Администрирование заявок и пользователей гостиницы">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="stylesheet" href="style.css">
|
|
<title>Панель управления</title>
|
|
</head>
|
|
<body>
|
|
<header></header>
|
|
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
|
|
<main>
|
|
<h1>Панель управления</h1>
|
|
|
|
<div class="card" style="margin-bottom: 2rem;">
|
|
<h2 style="margin-top:0;">Синхронизация заявок</h2>
|
|
<p style="color: var(--text-muted); margin-bottom: 1rem; font-size: 0.9375rem;">Получение актуальных данных из внешней системы бронирования</p>
|
|
<button id="syncBtn" class="btn-success">
|
|
<span id="syncBtnText">Запустить синхронизацию</span>
|
|
<span id="syncBtnSpinner" class="loading-spinner" style="display:none;"></span>
|
|
</button>
|
|
<p id="syncStatus" style="margin-top: 0.75rem; color: var(--text-secondary); font-size: 0.875rem;"></p>
|
|
</div>
|
|
|
|
<h2>Администраторы</h2>
|
|
|
|
<div class="table-container" style="margin-bottom: 2rem;">
|
|
<table id="adminsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Логин</th>
|
|
<th>Действия</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="adminsBody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3>Добавить администратора</h3>
|
|
<div class="form-card">
|
|
<form id="addAdminForm">
|
|
<div class="form-group">
|
|
<label for="newLogin">Логин</label>
|
|
<input type="text" id="newLogin" required placeholder="Минимум 3 символа" minlength="3">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="newPassword">Пароль</label>
|
|
<input type="password" id="newPassword" required placeholder="Минимум 6 символов" minlength="6">
|
|
</div>
|
|
|
|
<button type="submit">Добавить</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="editPasswordModal" class="modal">
|
|
<div class="modal-content">
|
|
<h2>Смена пароля</h2>
|
|
<form id="editPasswordForm">
|
|
<input type="hidden" id="editAdminId">
|
|
|
|
<div class="form-group">
|
|
<label>Администратор</label>
|
|
<input type="text" id="editAdminLogin" disabled style="opacity:0.6;">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="editPassword">Новый пароль</label>
|
|
<input type="password" id="editPassword" required placeholder="Минимум 6 символов" minlength="6">
|
|
</div>
|
|
|
|
<div style="display:flex;gap:0.75rem;margin-top:1.5rem;">
|
|
<button type="submit" style="flex:1;">Сохранить</button>
|
|
<button type="button" id="closePasswordModal" class="btn-secondary">Отмена</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script src="nav.js"></script>
|
|
<script src="seo.js"></script>
|
|
<script>
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
const container = document.getElementById('toastContainer');
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.innerHTML = `<span>${escapeHtml(message)}</span>`;
|
|
container.appendChild(toast);
|
|
setTimeout(() => {
|
|
toast.classList.add('toast-exit');
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
async function getCsrfToken() {
|
|
const stored = window.localStorage.getItem('csrfToken');
|
|
if (stored) return stored;
|
|
try {
|
|
const res = await fetch('/api/csrf-token');
|
|
const data = await res.json();
|
|
window.localStorage.setItem('csrfToken', data.csrfToken);
|
|
return data.csrfToken;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
fetch('/api/me').then(r => r.json()).then(data => {
|
|
if (!data.isAdmin) window.location.href = '/login.html';
|
|
});
|
|
|
|
document.getElementById('syncBtn').addEventListener('click', async () => {
|
|
const btn = document.getElementById('syncBtn');
|
|
const btnText = document.getElementById('syncBtnText');
|
|
const btnSpinner = document.getElementById('syncBtnSpinner');
|
|
const status = document.getElementById('syncStatus');
|
|
|
|
btn.disabled = true;
|
|
btnText.style.display = 'none';
|
|
btnSpinner.style.display = 'inline-block';
|
|
status.textContent = 'Синхронизация...';
|
|
|
|
try {
|
|
const csrfToken = await getCsrfToken();
|
|
const res = await fetch('/api/admin/sync', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': csrfToken }
|
|
});
|
|
if (res.ok) {
|
|
status.textContent = 'Синхронизация завершена успешно';
|
|
status.style.color = 'var(--success)';
|
|
showToast('Синхронизация завершена', 'success');
|
|
} else {
|
|
status.textContent = 'Ошибка синхронизации';
|
|
status.style.color = 'var(--danger)';
|
|
showToast('Ошибка синхронизации', 'error');
|
|
}
|
|
} catch (err) {
|
|
status.textContent = 'Ошибка соединения';
|
|
status.style.color = 'var(--danger)';
|
|
showToast('Ошибка соединения', 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btnText.style.display = 'inline';
|
|
btnSpinner.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
const adminsTbody = document.getElementById('adminsBody');
|
|
const addForm = document.getElementById('addAdminForm');
|
|
const passwordModal = document.getElementById('editPasswordModal');
|
|
const closePasswordBtn = document.getElementById('closePasswordModal');
|
|
|
|
async function loadAdmins() {
|
|
try {
|
|
const res = await fetch('/api/admins');
|
|
if (!res.ok) throw new Error('Ошибка загрузки');
|
|
const admins = await res.json();
|
|
|
|
adminsTbody.innerHTML = '';
|
|
if (admins.length === 0) {
|
|
adminsTbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:var(--text-muted);padding:2rem;">Нет администраторов</td></tr>';
|
|
return;
|
|
}
|
|
|
|
admins.forEach(admin => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${admin.id}</td>
|
|
<td><strong>${escapeHtml(admin.login)}</strong></td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="changePasswordBtn btn-secondary btn-sm" data-id="${admin.id}" data-login="${escapeHtml(admin.login)}">Сменить пароль</button>
|
|
<button class="deleteAdminBtn btn-danger btn-sm" data-id="${admin.id}">Удалить</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
adminsTbody.appendChild(tr);
|
|
});
|
|
|
|
document.querySelectorAll('.changePasswordBtn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.getElementById('editAdminId').value = btn.dataset.id;
|
|
document.getElementById('editAdminLogin').value = btn.dataset.login;
|
|
document.getElementById('editPassword').value = '';
|
|
passwordModal.classList.add('active');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.deleteAdminBtn').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
if (confirm('Удалить администратора? Это действие нельзя отменить.')) {
|
|
try {
|
|
const csrfToken = await getCsrfToken();
|
|
const res = await fetch(`/api/admins/${btn.dataset.id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-Token': csrfToken }
|
|
});
|
|
if (res.ok) {
|
|
showToast('Администратор удалён', 'success');
|
|
loadAdmins();
|
|
} else {
|
|
const err = await res.json();
|
|
showToast(err.error || 'Ошибка удаления', 'error');
|
|
}
|
|
} catch (err) {
|
|
showToast('Ошибка соединения', 'error');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
} catch (err) {
|
|
showToast('Ошибка загрузки', 'error');
|
|
}
|
|
}
|
|
|
|
addForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const login = document.getElementById('newLogin').value.trim();
|
|
const password = document.getElementById('newPassword').value;
|
|
|
|
try {
|
|
const csrfToken = await getCsrfToken();
|
|
const res = await fetch('/api/admins', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
|
|
body: JSON.stringify({ login, password })
|
|
});
|
|
if (res.ok) {
|
|
showToast('Администратор добавлен', 'success');
|
|
addForm.reset();
|
|
loadAdmins();
|
|
} else {
|
|
const err = await res.json();
|
|
showToast(err.error || 'Ошибка', 'error');
|
|
}
|
|
} catch (err) {
|
|
showToast('Ошибка соединения', 'error');
|
|
}
|
|
});
|
|
|
|
document.getElementById('editPasswordForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const id = document.getElementById('editAdminId').value;
|
|
const password = document.getElementById('editPassword').value;
|
|
|
|
try {
|
|
const csrfToken = await getCsrfToken();
|
|
const res = await fetch(`/api/admins/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
|
|
body: JSON.stringify({ password })
|
|
});
|
|
if (res.ok) {
|
|
showToast('Пароль изменён', 'success');
|
|
passwordModal.classList.remove('active');
|
|
loadAdmins();
|
|
} else {
|
|
const err = await res.json();
|
|
showToast(err.error || 'Ошибка', 'error');
|
|
}
|
|
} catch (err) {
|
|
showToast('Ошибка соединения', 'error');
|
|
}
|
|
});
|
|
|
|
closePasswordBtn.addEventListener('click', () => passwordModal.classList.remove('active'));
|
|
passwordModal.addEventListener('click', (e) => { if (e.target === passwordModal) passwordModal.classList.remove('active'); });
|
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && passwordModal.classList.contains('active')) passwordModal.classList.remove('active'); });
|
|
|
|
loadAdmins();
|
|
</script>
|
|
</body>
|
|
</html>
|