Files
yarmarka/templates/admin.html
2026-03-16 18:57:22 +03:00

687 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель | Rabota.Today</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
background: linear-gradient(145deg, #eef5fa 0%, #e0eaf5 100%);
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #0b1c34;
color: white;
padding: 20px 40px;
border-radius: 40px;
margin-bottom: 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
color: #3b82f6;
background: rgba(255,255,255,0.1);
padding: 12px;
border-radius: 20px;
}
.nav {
display: flex;
gap: 15px;
align-items: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 30px;
}
.nav a:hover {
background: rgba(255,255,255,0.1);
}
.nav .active {
background: #3b82f6;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: white;
border-radius: 30px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.stat-card .label {
color: #4f7092;
font-size: 14px;
margin-bottom: 10px;
}
.stat-card .value {
font-size: 36px;
font-weight: 700;
color: #0b1c34;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
background: white;
padding: 10px;
border-radius: 50px;
}
.tab {
padding: 12px 24px;
border-radius: 40px;
cursor: pointer;
font-weight: 600;
transition: 0.2s;
}
.tab.active {
background: #0b1c34;
color: white;
}
.content-card {
background: white;
border-radius: 40px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
}
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
padding: 15px;
background: #f0f7ff;
color: #1f3f60;
}
td {
padding: 15px;
border-bottom: 1px solid #dee9f5;
}
.badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge.employee {
background: #e0f2e0;
color: #166534;
}
.badge.employer {
background: #dbeafe;
color: #1e40af;
}
.badge.admin {
background: #fef3c7;
color: #92400e;
}
.btn-icon {
padding: 8px 12px;
border: none;
border-radius: 20px;
cursor: pointer;
margin: 0 5px;
}
.btn-icon.delete {
background: #fee2e2;
color: #b91c1c;
}
.btn-icon.edit {
background: #dbeafe;
color: #1e40af;
}
.tags-cloud {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0;
}
.tag-item {
background: #eef4fa;
padding: 8px 16px;
border-radius: 30px;
display: flex;
align-items: center;
gap: 8px;
}
.tag-item .count {
background: #3b82f6;
color: white;
padding: 2px 8px;
border-radius: 20px;
font-size: 12px;
}
.loading {
text-align: center;
padding: 40px;
color: #4f7092;
}
.modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
max-width: 500px;
width: 90%;
border-radius: 40px;
padding: 40px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1f3f60;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #dee9f5;
border-radius: 30px;
font-size: 16px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 30px;
font-weight: 600;
cursor: pointer;
}
.btn-primary {
background: #0b1c34;
color: white;
}
.btn-secondary {
background: #eef4fa;
color: #1f3f60;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">
<i class="fas fa-briefcase"></i>
Rabota.Today Admin
</div>
<div class="nav">
<a href="/">Главная</a>
<a href="/profile">Профиль</a>
<a href="/admin" class="active">Админка</a>
<a href="#" onclick="logout()">Выйти</a>
</div>
</div>
<!-- Статистика -->
<div class="stats-grid" id="stats">
<div class="stat-card">
<div class="label">Пользователи</div>
<div class="value" id="totalUsers">0</div>
</div>
<div class="stat-card">
<div class="label">Соискатели</div>
<div class="value" id="totalEmployees">0</div>
</div>
<div class="stat-card">
<div class="label">Работодатели</div>
<div class="value" id="totalEmployers">0</div>
</div>
<div class="stat-card">
<div class="label">Вакансии</div>
<div class="value" id="activeVacancies">0</div>
</div>
<div class="stat-card">
<div class="label">Резюме</div>
<div class="value" id="totalResumes">0</div>
</div>
<div class="stat-card">
<div class="label">Отклики</div>
<div class="value" id="totalApplications">0</div>
</div>
</div>
<!-- Табы -->
<div class="tabs">
<div class="tab active" onclick="switchTab('users')">Пользователи</div>
<div class="tab" onclick="switchTab('vacancies')">Вакансии</div>
<div class="tab" onclick="switchTab('resumes')">Резюме</div>
<div class="tab" onclick="switchTab('tags')">Теги</div>
</div>
<!-- Контент -->
<div class="content-card">
<!-- Пользователи -->
<div id="usersTab" style="display: block;">
<h2 style="margin-bottom: 20px;">Управление пользователями</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
<th>Email</th>
<th>Телефон</th>
<th>Роль</th>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="usersTable"></tbody>
</table>
</div>
<!-- Вакансии -->
<div id="vacanciesTab" style="display: none;">
<h2 style="margin-bottom: 20px;">Управление вакансиями</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Компания</th>
<th>Зарплата</th>
<th>Теги</th>
<th>Просмотры</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="vacanciesTable"></tbody>
</table>
</div>
<!-- Резюме -->
<div id="resumesTab" style="display: none;">
<h2 style="margin-bottom: 20px;">Управление резюме</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
<th>Должность</th>
<th>Зарплата</th>
<th>Теги</th>
<th>Просмотры</th>
<th>Обновлено</th>
</tr>
</thead>
<tbody id="resumesTable"></tbody>
</table>
</div>
<!-- Теги -->
<div id="tagsTab" style="display: none;">
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
<h2>Управление тегами</h2>
<button class="btn btn-primary" onclick="showAddTagModal()">
<i class="fas fa-plus"></i> Добавить тег
</button>
</div>
<div class="tags-cloud" id="tagsCloud"></div>
<h3 style="margin: 30px 0 20px;">Популярные теги</h3>
<table>
<thead>
<tr>
<th>Тег</th>
<th>Категория</th>
<th>Вакансий</th>
<th>Резюме</th>
<th>Всего</th>
</tr>
</thead>
<tbody id="popularTagsTable"></tbody>
</table>
</div>
</div>
</div>
<!-- Модальное окно добавления тега -->
<div class="modal" id="tagModal">
<div class="modal-content">
<h3 style="margin-bottom: 20px;">Новый тег</h3>
<div class="form-group">
<label>Название тега</label>
<input type="text" id="tagName" placeholder="Например: Python">
</div>
<div class="form-group">
<label>Категория</label>
<select id="tagCategory">
<option value="skill">Навык</option>
<option value="industry">Отрасль</option>
<option value="position">Должность</option>
<option value="other">Другое</option>
</select>
</div>
<div style="display: flex; gap: 10px;">
<button class="btn btn-primary" onclick="createTag()">Создать</button>
<button class="btn btn-secondary" onclick="closeTagModal()">Отмена</button>
</div>
</div>
</div>
<script>
const currentProtocol = window.location.protocol; // http: или https:
const currentHost = window.location.host; // yarmarka.rabota.today или IP:порт
let API_BASE_URL = `${currentProtocol}//${currentHost}/api`;
const token = localStorage.getItem('accessToken');
// Проверка авторизации
if (!token) {
window.location.href = '/login';
}
// Загрузка статистики
async function loadStats() {
try {
const response = await fetch(`${API_BASE_URL}/admin/stats`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const stats = await response.json();
document.getElementById('totalUsers').textContent = stats.total_users;
document.getElementById('totalEmployees').textContent = stats.total_employees;
document.getElementById('totalEmployers').textContent = stats.total_employers;
document.getElementById('activeVacancies').textContent = stats.active_vacancies;
document.getElementById('totalResumes').textContent = stats.total_resumes;
document.getElementById('totalApplications').textContent = stats.total_applications;
// Загружаем популярные теги
const tagsCloud = document.getElementById('tagsCloud');
tagsCloud.innerHTML = stats.popular_tags.map(t => `
<div class="tag-item">
${t.name}
<span class="count">${t.vacancy_count + t.resume_count}</span>
</div>
`).join('');
// Таблица популярных тегов
const popularTagsTable = document.getElementById('popularTagsTable');
popularTagsTable.innerHTML = stats.popular_tags.map(t => `
<tr>
<td>${t.name}</td>
<td>${t.category || '—'}</td>
<td>${t.vacancy_count}</td>
<td>${t.resume_count}</td>
<td>${t.vacancy_count + t.resume_count}</td>
</tr>
`).join('');
} catch (error) {
console.error('Error loading stats:', error);
}
}
// Загрузка пользователей
async function loadUsers() {
try {
const response = await fetch(`${API_BASE_URL}/admin/users`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const users = await response.json();
const table = document.getElementById('usersTable');
table.innerHTML = users.map(u => `
<tr>
<td>${u.id}</td>
<td>${escapeHtml(u.full_name)}</td>
<td>${escapeHtml(u.email)}</td>
<td>${escapeHtml(u.phone)}</td>
<td>
<span class="badge ${u.role}">
${u.role === 'employee' ? 'Соискатель' : u.role === 'employer' ? 'Работодатель' : 'Админ'}
</span>
</td>
<td>${new Date(u.created_at).toLocaleDateString()}</td>
<td>
${!u.is_admin ? `
<button class="btn-icon delete" onclick="deleteUser(${u.id})">
<i class="fas fa-trash"></i>
</button>
` : ''}
</td>
</tr>
`).join('');
} catch (error) {
console.error('Error loading users:', error);
}
}
// Загрузка вакансий
async function loadVacancies() {
try {
const response = await fetch(`${API_BASE_URL}/admin/vacancies`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const vacancies = await response.json();
const table = document.getElementById('vacanciesTable');
table.innerHTML = vacancies.map(v => `
<tr>
<td>${v.id}</td>
<td>${escapeHtml(v.title)}</td>
<td>${escapeHtml(v.company_name)}</td>
<td>${escapeHtml(v.salary || '—')}</td>
<td>
${(v.tags || []).map(t =>
`<span class="badge" style="background: #eef4fa; margin: 2px;">${t.name}</span>`
).join('')}
</td>
<td>${v.views}</td>
<td>${new Date(v.created_at).toLocaleDateString()}</td>
<td>
<button class="btn-icon delete" onclick="deleteVacancy(${v.id})">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
} catch (error) {
console.error('Error loading vacancies:', error);
}
}
// Удаление пользователя
async function deleteUser(userId) {
if (!confirm('Удалить пользователя? Это действие нельзя отменить.')) return;
try {
await fetch(`${API_BASE_URL}/admin/users/${userId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
loadUsers();
loadStats();
} catch (error) {
alert('Ошибка при удалении');
}
}
// Удаление вакансии
async function deleteVacancy(vacancyId) {
if (!confirm('Удалить вакансию?')) return;
try {
await fetch(`${API_BASE_URL}/admin/vacancies/${vacancyId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
loadVacancies();
loadStats();
} catch (error) {
alert('Ошибка при удалении');
}
}
// Создание тега
async function createTag() {
const name = document.getElementById('tagName').value;
const category = document.getElementById('tagCategory').value;
if (!name) {
alert('Введите название тега');
return;
}
try {
await fetch(`${API_BASE_URL}/tags`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ name, category })
});
closeTagModal();
loadStats();
document.getElementById('tagName').value = '';
} catch (error) {
alert('Ошибка при создании тега');
}
}
// Переключение табов
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.content-card > div').forEach(d => d.style.display = 'none');
if (tab === 'users') {
document.querySelector('.tab').classList.add('active');
document.getElementById('usersTab').style.display = 'block';
loadUsers();
} else if (tab === 'vacancies') {
document.querySelectorAll('.tab')[1].classList.add('active');
document.getElementById('vacanciesTab').style.display = 'block';
loadVacancies();
} else if (tab === 'resumes') {
document.querySelectorAll('.tab')[2].classList.add('active');
document.getElementById('resumesTab').style.display = 'block';
} else if (tab === 'tags') {
document.querySelectorAll('.tab')[3].classList.add('active');
document.getElementById('tagsTab').style.display = 'block';
}
}
// Модальное окно
function showAddTagModal() {
document.getElementById('tagModal').classList.add('active');
}
function closeTagModal() {
document.getElementById('tagModal').classList.remove('active');
}
// Выход
function logout() {
localStorage.removeItem('accessToken');
window.location.href = '/';
}
// Экранирование HTML
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Загрузка данных
loadStats();
loadUsers();
// Закрытие модалки по клику вне окна
window.onclick = function(event) {
const modal = document.getElementById('tagModal');
if (event.target === modal) {
modal.classList.remove('active');
}
};
</script>
</body>
</html>