Files
hotel777-manager/public/admin.html
2026-05-04 21:09:42 +05:00

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>