1504 lines
55 KiB
HTML
1504 lines
55 KiB
HTML
<!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;
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
}
|
||
|
||
.logo {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.logo i {
|
||
color: #3b82f6;
|
||
background: rgba(255,255,255,0.1);
|
||
padding: 12px;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.nav {
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.nav a {
|
||
color: white;
|
||
text-decoration: none;
|
||
padding: 10px 20px;
|
||
border-radius: 30px;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.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);
|
||
transition: 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.stat-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 15px 35px rgba(0,20,40,0.15);
|
||
}
|
||
|
||
.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;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 24px;
|
||
border-radius: 40px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
transition: 0.2s;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.tab:hover {
|
||
background: #eef4fa;
|
||
}
|
||
|
||
.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-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
gap: 15px;
|
||
}
|
||
|
||
.table-header h2 {
|
||
color: #0b1c34;
|
||
font-size: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.table-header h2 i {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.search-box {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.search-box input {
|
||
padding: 12px 20px;
|
||
border: 2px solid #dee9f5;
|
||
border-radius: 30px;
|
||
font-size: 14px;
|
||
width: 250px;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.search-box input:focus {
|
||
border-color: #3b82f6;
|
||
outline: none;
|
||
}
|
||
|
||
.table-container {
|
||
overflow-x: auto;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 800px;
|
||
}
|
||
|
||
th {
|
||
text-align: left;
|
||
padding: 15px;
|
||
background: #f0f7ff;
|
||
color: #1f3f60;
|
||
font-weight: 600;
|
||
}
|
||
|
||
td {
|
||
padding: 15px;
|
||
border-bottom: 1px solid #dee9f5;
|
||
}
|
||
|
||
tr:hover td {
|
||
background: #f9fcff;
|
||
}
|
||
|
||
.badge {
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
display: inline-block;
|
||
}
|
||
|
||
.badge.employee {
|
||
background: #e0f2e0;
|
||
color: #166534;
|
||
}
|
||
|
||
.badge.employer {
|
||
background: #dbeafe;
|
||
color: #1e40af;
|
||
}
|
||
|
||
.badge.admin {
|
||
background: #fef3c7;
|
||
color: #92400e;
|
||
}
|
||
|
||
.badge.active {
|
||
background: #d1fae5;
|
||
color: #065f46;
|
||
}
|
||
|
||
.badge.inactive {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.btn-icon {
|
||
padding: 8px 12px;
|
||
border: none;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
margin: 0 5px;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.btn-icon.delete {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.btn-icon.delete:hover {
|
||
background: #fecaca;
|
||
}
|
||
|
||
.btn-icon.edit {
|
||
background: #dbeafe;
|
||
color: #1e40af;
|
||
}
|
||
|
||
.btn-icon.edit:hover {
|
||
background: #bfdbfe;
|
||
}
|
||
|
||
.btn-icon.view {
|
||
background: #e0f2fe;
|
||
color: #0369a1;
|
||
}
|
||
|
||
.btn-icon.view:hover {
|
||
background: #bae6fd;
|
||
}
|
||
|
||
.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;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.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.6);
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
max-width: 600px;
|
||
width: 90%;
|
||
border-radius: 40px;
|
||
padding: 40px;
|
||
position: relative;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-close {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: #eef4fa;
|
||
border: none;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 20px;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #4f7092;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
background: #dbeafe;
|
||
color: #0b1c34;
|
||
}
|
||
|
||
.modal-content h2 {
|
||
color: #0b1c34;
|
||
margin-bottom: 20px;
|
||
padding-right: 30px;
|
||
}
|
||
|
||
.detail-section {
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.detail-section h3 {
|
||
color: #1f3f60;
|
||
margin-bottom: 10px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.detail-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 15px;
|
||
}
|
||
|
||
.detail-item {
|
||
padding: 10px;
|
||
background: #f9fcff;
|
||
border-radius: 15px;
|
||
}
|
||
|
||
.detail-item .label {
|
||
font-size: 12px;
|
||
color: #4f7092;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.detail-item .value {
|
||
font-weight: 600;
|
||
color: #0b1c34;
|
||
}
|
||
|
||
.experience-list, .education-list {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.exp-item, .edu-item {
|
||
background: #f9fcff;
|
||
padding: 15px;
|
||
border-radius: 15px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.exp-item strong, .edu-item strong {
|
||
color: #0b1c34;
|
||
}
|
||
|
||
.exp-item .period, .edu-item .year {
|
||
color: #4f7092;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 16px 24px;
|
||
border-radius: 30px;
|
||
background: white;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||
z-index: 9999;
|
||
animation: slideIn 0.3s;
|
||
max-width: 350px;
|
||
display: none;
|
||
}
|
||
|
||
.notification.success {
|
||
background: #10b981;
|
||
color: white;
|
||
}
|
||
|
||
.notification.error {
|
||
background: #ef4444;
|
||
color: white;
|
||
}
|
||
|
||
.notification.info {
|
||
background: #3b82f6;
|
||
color: white;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0%); opacity: 1; }
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
margin-top: 30px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.page-btn {
|
||
padding: 8px 16px;
|
||
border: 2px solid #dee9f5;
|
||
background: white;
|
||
border-radius: 30px;
|
||
cursor: pointer;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.page-btn:hover {
|
||
background: #eef4fa;
|
||
}
|
||
|
||
.page-btn.active {
|
||
background: #0b1c34;
|
||
color: white;
|
||
border-color: #0b1c34;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.detail-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Стили для ссылок на профили */
|
||
.user-link {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #0b1c34;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
padding: 4px 8px;
|
||
border-radius: 30px;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.user-link:hover {
|
||
background: #eef4fa;
|
||
color: #0b1c34;
|
||
}
|
||
|
||
.user-link i {
|
||
color: #3b82f6;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.user-link .external-icon {
|
||
font-size: 12px;
|
||
color: #4f7092;
|
||
opacity: 0.5;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.user-link:hover .external-icon {
|
||
opacity: 1;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
/* Стили для кнопок действий */
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 5px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.btn-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 16px;
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: 0.2s;
|
||
background: transparent;
|
||
color: #4f7092;
|
||
}
|
||
|
||
.btn-icon:hover {
|
||
background: #eef4fa;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.btn-icon.view:hover {
|
||
background: #dbeafe;
|
||
color: #1e40af;
|
||
}
|
||
|
||
.btn-icon.delete:hover {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
/* Email ссылка */
|
||
.email-link {
|
||
color: #3b82f6;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.email-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<div class="logo" onclick="window.location.href='/'">
|
||
<i class="fas fa-briefcase"></i>
|
||
Rabota.Today Admin
|
||
</div>
|
||
<div class="nav" id="nav">
|
||
<!-- Навигация -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Статистика -->
|
||
<div class="stats-grid" id="stats">
|
||
<div class="stat-card" onclick="filterByType('users')">
|
||
<div class="label">Пользователи</div>
|
||
<div class="value" id="totalUsers">0</div>
|
||
</div>
|
||
<div class="stat-card" onclick="filterByType('employees')">
|
||
<div class="label">Соискатели</div>
|
||
<div class="value" id="totalEmployees">0</div>
|
||
</div>
|
||
<div class="stat-card" onclick="filterByType('employers')">
|
||
<div class="label">Работодатели</div>
|
||
<div class="value" id="totalEmployers">0</div>
|
||
</div>
|
||
<div class="stat-card" onclick="filterByType('vacancies')">
|
||
<div class="label">Вакансии</div>
|
||
<div class="value" id="activeVacancies">0</div>
|
||
</div>
|
||
<div class="stat-card" onclick="filterByType('resumes')">
|
||
<div class="label">Резюме</div>
|
||
<div class="value" id="totalResumes">0</div>
|
||
</div>
|
||
<div class="stat-card" onclick="filterByType('applications')">
|
||
<div class="label">Отклики</div>
|
||
<div class="value" id="totalApplications">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Табы -->
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('users')">Пользователи</button>
|
||
<button class="tab" onclick="switchTab('vacancies')">Вакансии</button>
|
||
<button class="tab" onclick="switchTab('resumes')">Резюме</button>
|
||
<button class="tab" onclick="switchTab('tags')">Теги</button>
|
||
</div>
|
||
|
||
<!-- Контент -->
|
||
<div class="content-card">
|
||
<!-- Пользователи -->
|
||
<div id="usersTab">
|
||
<div class="table-header">
|
||
<h2><i class="fas fa-users"></i> Управление пользователями</h2>
|
||
<div class="search-box">
|
||
<input type="text" id="userSearch" placeholder="Поиск по email или имени..." oninput="debounceSearch('users')">
|
||
</div>
|
||
</div>
|
||
<div class="table-container">
|
||
<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 class="pagination" id="usersPagination"></div>
|
||
</div>
|
||
|
||
<!-- Вакансии -->
|
||
<div id="vacanciesTab" style="display: none;">
|
||
<div class="table-header">
|
||
<h2><i class="fas fa-briefcase"></i> Управление вакансиями</h2>
|
||
<div class="search-box">
|
||
<input type="text" id="vacancySearch" placeholder="Поиск по названию..." oninput="debounceSearch('vacancies')">
|
||
</div>
|
||
</div>
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Название</th>
|
||
<th>Компания</th>
|
||
<th>Зарплата</th>
|
||
<th>Теги</th>
|
||
<th>Статус</th>
|
||
<th>Просмотры</th>
|
||
<th>Дата</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="vacanciesTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination" id="vacanciesPagination"></div>
|
||
</div>
|
||
|
||
<!-- Резюме -->
|
||
<div id="resumesTab" style="display: none;">
|
||
<div class="table-header">
|
||
<h2><i class="fas fa-file-alt"></i> Управление резюме</h2>
|
||
<div class="search-box">
|
||
<input type="text" id="resumeSearch" placeholder="Поиск по имени или должности..." oninput="debounceSearch('resumes')">
|
||
</div>
|
||
</div>
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Имя</th>
|
||
<th>Желаемая должность</th>
|
||
<th>Зарплата</th>
|
||
<th>Опыт</th>
|
||
<th>Теги</th>
|
||
<th>Просмотры</th>
|
||
<th>Обновлено</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="resumesTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination" id="resumesPagination"></div>
|
||
</div>
|
||
|
||
<!-- Теги -->
|
||
<div id="tagsTab" style="display: none;">
|
||
<div class="table-header">
|
||
<h2><i class="fas fa-tags"></i> Управление тегами</h2>
|
||
<button class="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>
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Тег</th>
|
||
<th>Категория</th>
|
||
<th>Вакансий</th>
|
||
<th>Резюме</th>
|
||
<th>Всего</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="popularTagsTable"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно добавления тега -->
|
||
<div class="modal" id="tagModal">
|
||
<div class="modal-content">
|
||
<button class="modal-close" onclick="closeTagModal()">×</button>
|
||
<h2>Новый тег</h2>
|
||
|
||
<div class="detail-section">
|
||
<label style="display: block; margin-bottom: 8px; font-weight: 600;">Название тега</label>
|
||
<input type="text" id="tagName" placeholder="Например: Python" style="width: 100%; padding: 12px; border: 2px solid #dee9f5; border-radius: 30px;">
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<label style="display: block; margin-bottom: 8px; font-weight: 600;">Категория</label>
|
||
<select id="tagCategory" style="width: 100%; padding: 12px; border: 2px solid #dee9f5; border-radius: 30px;">
|
||
<option value="skill">Навык</option>
|
||
<option value="industry">Отрасль</option>
|
||
<option value="position">Должность</option>
|
||
<option value="other">Другое</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; margin-top: 30px;">
|
||
<button class="btn-primary" onclick="createTag()" style="flex: 1; padding: 14px;">Создать</button>
|
||
<button class="btn-secondary" onclick="closeTagModal()" style="flex: 1; padding: 14px;">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно просмотра резюме -->
|
||
<div class="modal" id="resumeViewModal">
|
||
<div class="modal-content">
|
||
<button class="modal-close" onclick="closeResumeModal()">×</button>
|
||
<h2 id="resumeModalName"></h2>
|
||
|
||
<div id="resumeModalContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Уведомления -->
|
||
<div class="notification" id="notification"></div>
|
||
|
||
<script>
|
||
const API_BASE_URL = window.location.protocol + '//' + window.location.host + '/api';
|
||
const token = localStorage.getItem('accessToken');
|
||
|
||
// Проверка авторизации
|
||
if (!token) {
|
||
window.location.href = '/login';
|
||
}
|
||
|
||
// Переменные для пагинации
|
||
let currentPage = {
|
||
users: 1,
|
||
vacancies: 1,
|
||
resumes: 1
|
||
};
|
||
|
||
let searchTerms = {
|
||
users: '',
|
||
vacancies: '',
|
||
resumes: ''
|
||
};
|
||
|
||
let searchTimeouts = {};
|
||
|
||
// ========== УВЕДОМЛЕНИЯ ==========
|
||
function showNotification(message, type = 'success') {
|
||
const notification = document.getElementById('notification');
|
||
notification.className = `notification ${type}`;
|
||
notification.innerHTML = message;
|
||
notification.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
notification.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
// ========== ЗАГРУЗКА СТАТИСТИКИ ==========
|
||
async function loadStats() {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/admin/stats`, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки статистики');
|
||
|
||
const stats = await response.json();
|
||
|
||
document.getElementById('totalUsers').textContent = stats.total_users || 0;
|
||
document.getElementById('totalEmployees').textContent = stats.total_employees || 0;
|
||
document.getElementById('totalEmployers').textContent = stats.total_employers || 0;
|
||
document.getElementById('activeVacancies').textContent = stats.active_vacancies || 0;
|
||
document.getElementById('totalResumes').textContent = stats.total_resumes || 0;
|
||
document.getElementById('totalApplications').textContent = stats.total_applications || 0;
|
||
|
||
// Загружаем популярные теги
|
||
const popularTagsTable = document.getElementById('popularTagsTable');
|
||
if (stats.popular_tags && stats.popular_tags.length > 0) {
|
||
popularTagsTable.innerHTML = stats.popular_tags.map(t => `
|
||
<tr>
|
||
<td><strong>${escapeHtml(t.name)}</strong></td>
|
||
<td>${t.category || '—'}</td>
|
||
<td>${t.vacancy_count || 0}</td>
|
||
<td>${t.resume_count || 0}</td>
|
||
<td>${(t.vacancy_count || 0) + (t.resume_count || 0)}</td>
|
||
</tr>
|
||
`).join('');
|
||
} else {
|
||
popularTagsTable.innerHTML = '<tr><td colspan="5" style="text-align: center;">Нет данных</td></tr>';
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error loading stats:', error);
|
||
showNotification('Ошибка загрузки статистики', 'error');
|
||
}
|
||
}
|
||
|
||
// ========== ЗАГРУЗКА ПОЛЬЗОВАТЕЛЕЙ ==========
|
||
async function loadUsers(page = 1, search = '') {
|
||
try {
|
||
console.log(`📥 Загрузка пользователей: страница ${page}, поиск "${search}"`);
|
||
console.log('🔑 Токен:', token ? 'Есть' : 'Нет');
|
||
|
||
let url = `${API_BASE_URL}/admin/users?page=${page}&limit=10`;
|
||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||
|
||
const response = await fetch(url, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error('❌ Ошибка ответа:', response.status, response.statusText);
|
||
throw new Error('Ошибка загрузки пользователей');
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('✅ Данные пользователей:', data);
|
||
|
||
const users = data.users || data;
|
||
const totalPages = data.total_pages || Math.ceil((data.total || users.length) / 10) || 1;
|
||
|
||
const table = document.getElementById('usersTable');
|
||
|
||
if (!users || users.length === 0) {
|
||
table.innerHTML = '<tr><td colspan="7" style="text-align: center;">Пользователи не найдены</td></tr>';
|
||
return;
|
||
}
|
||
|
||
table.innerHTML = users.map(u => `
|
||
<tr>
|
||
<td>${u.id}</td>
|
||
<td>
|
||
<a href="/user/${u.id}" target="_blank" style="color: #0b1c34; text-decoration: none; font-weight: 600; display: flex; align-items: center; gap: 5px;">
|
||
<i class="fas fa-user-circle" style="color: #3b82f6;"></i>
|
||
${escapeHtml(u.full_name)}
|
||
<i class="fas fa-external-link-alt" style="font-size: 12px; color: #4f7092; opacity: 0.5;"></i>
|
||
</a>
|
||
</td>
|
||
<td>
|
||
<a href="mailto:${escapeHtml(u.email)}" style="color: #3b82f6; text-decoration: none;">
|
||
${escapeHtml(u.email)}
|
||
</a>
|
||
</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>
|
||
<div style="display: flex; gap: 5px;">
|
||
<button class="btn-icon view" onclick="window.open('/user/${u.id}', '_blank')" title="Просмотр профиля">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
${!u.is_admin ? `
|
||
<button class="btn-icon delete" onclick="deleteUser(${u.id})" title="Удалить">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
renderPagination('users', totalPages, page);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка загрузки пользователей:', error);
|
||
document.getElementById('usersTable').innerHTML = '<tr><td colspan="7" style="text-align: center;">Ошибка загрузки</td></tr>';
|
||
}
|
||
}
|
||
|
||
// ========== ЗАГРУЗКА ВАКАНСИЙ ==========
|
||
async function loadVacancies(page = 1, search = '') {
|
||
try {
|
||
let url = `${API_BASE_URL}/admin/vacancies?page=${page}`;
|
||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||
|
||
const response = await fetch(url, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки вакансий');
|
||
|
||
const data = await response.json();
|
||
const vacancies = data.vacancies || data;
|
||
|
||
const table = document.getElementById('vacanciesTable');
|
||
|
||
if (!vacancies || vacancies.length === 0) {
|
||
table.innerHTML = '<tr><td colspan="9" style="text-align: center;">Вакансии не найдены</td></tr>';
|
||
return;
|
||
}
|
||
|
||
table.innerHTML = vacancies.map(v => `
|
||
<tr>
|
||
<td>${v.id}</td>
|
||
<td><strong>${escapeHtml(v.title)}</strong></td>
|
||
<td>${escapeHtml(v.company_name || '—')}</td>
|
||
<td>${escapeHtml(v.salary || '—')}</td>
|
||
<td>
|
||
${(v.tags || []).map(t =>
|
||
`<span class="badge" style="background: #eef4fa; color: #1f3f60; margin: 2px;">${escapeHtml(t.name)}</span>`
|
||
).join('')}
|
||
</td>
|
||
<td>
|
||
<span class="badge ${v.is_active ? 'active' : 'inactive'}">
|
||
${v.is_active ? 'Активна' : 'Неактивна'}
|
||
</span>
|
||
</td>
|
||
<td>${v.views || 0}</td>
|
||
<td>${new Date(v.created_at).toLocaleDateString()}</td>
|
||
<td>
|
||
<button class="btn-icon view" onclick="viewVacancy(${v.id})" title="Просмотр">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
<button class="btn-icon delete" onclick="deleteVacancy(${v.id})" title="Удалить">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
renderPagination('vacancies', data.total_pages || 1, page);
|
||
|
||
} catch (error) {
|
||
console.error('Error loading vacancies:', error);
|
||
document.getElementById('vacanciesTable').innerHTML = '<tr><td colspan="9" style="text-align: center;">Ошибка загрузки</td></tr>';
|
||
}
|
||
}
|
||
|
||
// ========== ЗАГРУЗКА РЕЗЮМЕ ==========
|
||
async function loadResumes(page = 1, search = '') {
|
||
try {
|
||
console.log(`📥 Загрузка резюме: страница ${page}, поиск "${search}"`);
|
||
|
||
let url = `${API_BASE_URL}/admin/resumes?page=${page}&limit=10`;
|
||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||
|
||
const response = await fetch(url, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.warn('Эндпоинт admin/resumes не найден, пробуем резервный вариант');
|
||
// Пробуем загрузить через публичный эндпоинт
|
||
await loadResumesFallback(page, search);
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('✅ Резюме загружены:', data);
|
||
|
||
const resumes = data.resumes || data;
|
||
const totalPages = data.total_pages || Math.ceil((data.total || resumes.length) / 10) || 1;
|
||
|
||
const table = document.getElementById('resumesTable');
|
||
|
||
if (!resumes || resumes.length === 0) {
|
||
table.innerHTML = '<tr><td colspan="9" style="text-align: center;">Резюме не найдены</td></tr>';
|
||
return;
|
||
}
|
||
|
||
table.innerHTML = resumes.map(r => {
|
||
// Безопасное получение опыта
|
||
const expCount = r.work_experience ? r.work_experience.length : (r.experience_count || 0);
|
||
|
||
return `
|
||
<tr>
|
||
<td>${r.id}</td>
|
||
<td><strong>${escapeHtml(r.full_name || '—')}</strong></td>
|
||
<td>${escapeHtml(r.desired_position || '—')}</td>
|
||
<td>${escapeHtml(r.desired_salary || '—')}</td>
|
||
<td>${expCount} ${getDeclension(expCount, ['место', 'места', 'мест'])}</td>
|
||
<td>
|
||
${(r.tags || []).map(t =>
|
||
`<span class="badge" style="background: #eef4fa; color: #1f3f60; margin: 2px;">${escapeHtml(t.name || t)}</span>`
|
||
).join('')}
|
||
</td>
|
||
<td>${r.views || 0}</td>
|
||
<td>${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'}</td>
|
||
<td>
|
||
<button class="btn-icon view" onclick="viewResume(${r.id})" title="Просмотр">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
<button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`}).join('');
|
||
|
||
renderPagination('resumes', totalPages, page);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка загрузки резюме:', error);
|
||
document.getElementById('resumesTable').innerHTML = '<tr><td colspan="9" style="text-align: center;">Ошибка загрузки резюме</td></tr>';
|
||
}
|
||
}
|
||
|
||
// Резервный метод загрузки резюме
|
||
async function loadResumesFallback(page = 1, search = '') {
|
||
try {
|
||
console.log('🔄 Используем резервный метод загрузки резюме');
|
||
|
||
let url = `${API_BASE_URL}/resumes/all?page=${page}&limit=10`;
|
||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||
|
||
const response = await fetch(url);
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки резюме');
|
||
|
||
const data = await response.json();
|
||
console.log('✅ Резюме загружены (резервный метод):', data);
|
||
|
||
const resumes = data.resumes || [];
|
||
const totalPages = data.total_pages || 1;
|
||
|
||
const table = document.getElementById('resumesTable');
|
||
|
||
if (!resumes || resumes.length === 0) {
|
||
table.innerHTML = '<tr><td colspan="9" style="text-align: center;">Резюме не найдены</td></tr>';
|
||
return;
|
||
}
|
||
|
||
table.innerHTML = resumes.map(r => {
|
||
const expCount = r.experience_count || 0;
|
||
|
||
return `
|
||
<tr>
|
||
<td>${r.id}</td>
|
||
<td><strong>${escapeHtml(r.full_name || '—')}</strong></td>
|
||
<td>${escapeHtml(r.desired_position || '—')}</td>
|
||
<td>${escapeHtml(r.desired_salary || '—')}</td>
|
||
<td>${expCount} ${getDeclension(expCount, ['место', 'места', 'мест'])}</td>
|
||
<td>
|
||
${(r.tags || []).map(t =>
|
||
`<span class="badge" style="background: #eef4fa; color: #1f3f60; margin: 2px;">${escapeHtml(t.name || t)}</span>`
|
||
).join('')}
|
||
</td>
|
||
<td>${r.views || 0}</td>
|
||
<td>${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'}</td>
|
||
<td>
|
||
<button class="btn-icon view" onclick="viewResume(${r.id})" title="Просмотр">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
<button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`}).join('');
|
||
|
||
renderPagination('resumes', totalPages, page);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка резервной загрузки резюме:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Вспомогательная функция для склонения
|
||
function getDeclension(number, titles) {
|
||
const cases = [2, 0, 1, 1, 1, 2];
|
||
return titles[ (number % 100 > 4 && number % 100 < 20) ? 2 : cases[(number % 10 < 5) ? number % 10 : 5] ];
|
||
}
|
||
|
||
// ========== ПРОСМОТР РЕЗЮМЕ ==========
|
||
async function viewResume(resumeId) {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/resumes/${resumeId}`);
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки резюме');
|
||
|
||
const resume = await response.json();
|
||
|
||
document.getElementById('resumeModalName').textContent = escapeHtml(resume.full_name || 'Резюме');
|
||
|
||
const experienceHtml = resume.work_experience && resume.work_experience.length > 0
|
||
? resume.work_experience.map(exp => `
|
||
<div class="exp-item">
|
||
<strong>${escapeHtml(exp.position)}</strong> <span class="period">${escapeHtml(exp.period || '')}</span><br>
|
||
<span>${escapeHtml(exp.company)}</span>
|
||
${exp.description ? `<p style="margin-top: 8px; font-size: 13px;">${escapeHtml(exp.description.substring(0, 100))}...</p>` : ''}
|
||
</div>
|
||
`).join('')
|
||
: '<p>Опыт работы не указан</p>';
|
||
|
||
const educationHtml = resume.education && resume.education.length > 0
|
||
? resume.education.map(edu => `
|
||
<div class="edu-item">
|
||
<strong>${escapeHtml(edu.institution)}</strong> <span class="year">${escapeHtml(edu.graduation_year || '')}</span><br>
|
||
<span>${escapeHtml(edu.specialty || '')}</span>
|
||
</div>
|
||
`).join('')
|
||
: '<p>Образование не указано</p>';
|
||
|
||
document.getElementById('resumeModalContent').innerHTML = `
|
||
<div class="detail-section">
|
||
<h3>Основная информация</h3>
|
||
<div class="detail-grid">
|
||
<div class="detail-item">
|
||
<div class="label">Желаемая должность</div>
|
||
<div class="value">${escapeHtml(resume.desired_position || '—')}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Зарплата</div>
|
||
<div class="value">${escapeHtml(resume.desired_salary || '—')}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Просмотров</div>
|
||
<div class="value">${resume.views || 0}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Обновлено</div>
|
||
<div class="value">${resume.updated_at ? new Date(resume.updated_at).toLocaleDateString() : '—'}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>О себе</h3>
|
||
<p>${escapeHtml(resume.about_me || 'Информация не заполнена')}</p>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>Опыт работы</h3>
|
||
<div class="experience-list">
|
||
${experienceHtml}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>Образование</h3>
|
||
<div class="education-list">
|
||
${educationHtml}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>Контакты</h3>
|
||
<div class="detail-grid">
|
||
<div class="detail-item">
|
||
<div class="label">Email</div>
|
||
<div class="value">${escapeHtml(resume.email || '—')}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Телефон</div>
|
||
<div class="value">${escapeHtml(resume.phone || '—')}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Telegram</div>
|
||
<div class="value">${escapeHtml(resume.telegram || '—')}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('resumeViewModal').classList.add('active');
|
||
|
||
} catch (error) {
|
||
console.error('Error loading resume details:', error);
|
||
showNotification('Ошибка загрузки деталей резюме', 'error');
|
||
}
|
||
}
|
||
|
||
function closeResumeModal() {
|
||
document.getElementById('resumeViewModal').classList.remove('active');
|
||
}
|
||
|
||
// ========== УДАЛЕНИЕ РЕЗЮМЕ ==========
|
||
async function deleteResume(resumeId) {
|
||
if (!confirm('Вы уверены, что хотите удалить это резюме? Это действие нельзя отменить.')) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/admin/resumes/${resumeId}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка удаления');
|
||
|
||
showNotification('Резюме удалено', 'success');
|
||
loadResumes(currentPage.resumes, searchTerms.resumes);
|
||
loadStats(); // Обновляем статистику
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting resume:', error);
|
||
showNotification('Ошибка при удалении резюме', 'error');
|
||
}
|
||
}
|
||
|
||
// ========== УДАЛЕНИЕ ПОЛЬЗОВАТЕЛЯ ==========
|
||
async function deleteUser(userId) {
|
||
if (!confirm('Удалить пользователя? Это действие нельзя отменить.')) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/admin/users/${userId}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка удаления');
|
||
|
||
showNotification('Пользователь удален', 'success');
|
||
loadUsers(currentPage.users, searchTerms.users);
|
||
loadStats();
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting user:', error);
|
||
showNotification('Ошибка при удалении', 'error');
|
||
}
|
||
}
|
||
|
||
// ========== УДАЛЕНИЕ ВАКАНСИИ ==========
|
||
async function deleteVacancy(vacancyId) {
|
||
if (!confirm('Удалить вакансию?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/admin/vacancies/${vacancyId}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка удаления');
|
||
|
||
showNotification('Вакансия удалена', 'success');
|
||
loadVacancies(currentPage.vacancies, searchTerms.vacancies);
|
||
loadStats();
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting vacancy:', error);
|
||
showNotification('Ошибка при удалении', 'error');
|
||
}
|
||
}
|
||
|
||
// ========== ПРОСМОТР ВАКАНСИИ ==========
|
||
function viewVacancy(vacancyId) {
|
||
window.open(`/vacancy/${vacancyId}`, '_blank');
|
||
}
|
||
|
||
// ========== ПАГИНАЦИЯ ==========
|
||
function renderPagination(type, totalPages, currentPage) {
|
||
const container = document.getElementById(`${type}Pagination`);
|
||
if (!container) return;
|
||
|
||
if (totalPages <= 1) {
|
||
container.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
for (let i = 1; i <= totalPages; i++) {
|
||
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="goToPage('${type}', ${i})">${i}</button>`;
|
||
}
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function goToPage(type, page) {
|
||
currentPage[type] = page;
|
||
|
||
switch(type) {
|
||
case 'users':
|
||
loadUsers(page, searchTerms.users);
|
||
break;
|
||
case 'vacancies':
|
||
loadVacancies(page, searchTerms.vacancies);
|
||
break;
|
||
case 'resumes':
|
||
loadResumes(page, searchTerms.resumes);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ========== ПОИСК С DEBOUNCE ==========
|
||
function debounceSearch(type) {
|
||
clearTimeout(searchTimeouts[type]);
|
||
|
||
searchTimeouts[type] = setTimeout(() => {
|
||
const searchInput = document.getElementById(`${type}Search`);
|
||
if (searchInput) {
|
||
searchTerms[type] = searchInput.value;
|
||
currentPage[type] = 1;
|
||
|
||
switch(type) {
|
||
case 'users':
|
||
loadUsers(1, searchTerms[type]);
|
||
break;
|
||
case 'vacancies':
|
||
loadVacancies(1, searchTerms[type]);
|
||
break;
|
||
case 'resumes':
|
||
loadResumes(1, searchTerms[type]);
|
||
break;
|
||
}
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
// ========== ПЕРЕКЛЮЧЕНИЕ ТАБОВ ==========
|
||
function switchTab(tab) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.content-card > div').forEach(d => d.style.display = 'none');
|
||
|
||
const activeTab = document.querySelector(`.tab:nth-child(${tab === 'users' ? 1 : tab === 'vacancies' ? 2 : tab === 'resumes' ? 3 : 4})`);
|
||
if (activeTab) activeTab.classList.add('active');
|
||
|
||
if (tab === 'users') {
|
||
document.getElementById('usersTab').style.display = 'block';
|
||
loadUsers(currentPage.users, searchTerms.users);
|
||
} else if (tab === 'vacancies') {
|
||
document.getElementById('vacanciesTab').style.display = 'block';
|
||
loadVacancies(currentPage.vacancies, searchTerms.vacancies);
|
||
} else if (tab === 'resumes') {
|
||
document.getElementById('resumesTab').style.display = 'block';
|
||
loadResumes(currentPage.resumes, searchTerms.resumes);
|
||
} else if (tab === 'tags') {
|
||
document.getElementById('tagsTab').style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// ========== ФИЛЬТРАЦИЯ ПО СТАТИСТИКЕ ==========
|
||
function filterByType(type) {
|
||
if (type === 'users') {
|
||
switchTab('users');
|
||
} else if (type === 'employees') {
|
||
switchTab('users');
|
||
// Здесь можно добавить фильтр по роли
|
||
} else if (type === 'employers') {
|
||
switchTab('users');
|
||
} else if (type === 'vacancies') {
|
||
switchTab('vacancies');
|
||
} else if (type === 'resumes') {
|
||
switchTab('resumes');
|
||
} else if (type === 'applications') {
|
||
window.location.href = '/applications';
|
||
}
|
||
}
|
||
|
||
// ========== РАБОТА С ТЕГАМИ ==========
|
||
function showAddTagModal() {
|
||
document.getElementById('tagModal').classList.add('active');
|
||
}
|
||
|
||
function closeTagModal() {
|
||
document.getElementById('tagModal').classList.remove('active');
|
||
document.getElementById('tagName').value = '';
|
||
}
|
||
|
||
async function createTag() {
|
||
const name = document.getElementById('tagName').value.trim();
|
||
const category = document.getElementById('tagCategory').value;
|
||
|
||
if (!name) {
|
||
showNotification('Введите название тега', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/tags`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ name, category })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Ошибка создания тега');
|
||
}
|
||
|
||
showNotification('Тег создан', 'success');
|
||
closeTagModal();
|
||
loadStats(); // Обновляем статистику
|
||
|
||
} catch (error) {
|
||
console.error('Error creating tag:', error);
|
||
showNotification(error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ========== ЭКРАНИРОВАНИЕ HTML ==========
|
||
function escapeHtml(unsafe) {
|
||
if (!unsafe) return '';
|
||
return unsafe.toString()
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// ========== НАВИГАЦИЯ ==========
|
||
function updateNavigation() {
|
||
const nav = document.getElementById('nav');
|
||
nav.innerHTML = `
|
||
<a href="/">Главная</a>
|
||
<a href="/vacancies">Вакансии</a>
|
||
<a href="/resumes">Резюме</a>
|
||
<a href="/favorites">Избранное</a>
|
||
<a href="/applications">Отклики</a>
|
||
<a href="/profile">Профиль</a>
|
||
<a href="#" onclick="logout()">Выйти</a>
|
||
`;
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('accessToken');
|
||
window.location.href = '/';
|
||
}
|
||
|
||
// ========== ЗАКРЫТИЕ МОДАЛОК ПО КЛИКУ ВНЕ ==========
|
||
window.onclick = function(event) {
|
||
const tagModal = document.getElementById('tagModal');
|
||
const resumeModal = document.getElementById('resumeViewModal');
|
||
|
||
if (event.target === tagModal) {
|
||
tagModal.classList.remove('active');
|
||
}
|
||
if (event.target === resumeModal) {
|
||
resumeModal.classList.remove('active');
|
||
}
|
||
};
|
||
|
||
// ========== ИНИЦИАЛИЗАЦИЯ ==========
|
||
window.addEventListener('load', () => {
|
||
updateNavigation();
|
||
loadStats();
|
||
loadUsers();
|
||
// Не загружаем все сразу, только активный таб
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |