2333 lines
96 KiB
HTML
2333 lines
96 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;
|
||
}
|
||
|
||
.company-logo {
|
||
width: 40px;
|
||
height: 40px;
|
||
background: #eef4fa;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
color: #3b82f6;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.company-logo img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* Стили для форм редактирования */
|
||
.edit-form {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
font-weight: 600;
|
||
color: #1f3f60;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group textarea,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border: 2px solid #dee9f5;
|
||
border-radius: 30px;
|
||
font-size: 14px;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group textarea:focus,
|
||
.form-group select:focus {
|
||
border-color: #3b82f6;
|
||
outline: none;
|
||
}
|
||
|
||
.form-group textarea {
|
||
min-height: 100px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 15px;
|
||
}
|
||
|
||
.checkbox-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.checkbox-group input {
|
||
width: auto;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.tags-input {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 8px;
|
||
border: 2px solid #dee9f5;
|
||
border-radius: 30px;
|
||
background: white;
|
||
min-height: 50px;
|
||
}
|
||
|
||
.tag-input-item {
|
||
background: #eef4fa;
|
||
padding: 4px 12px;
|
||
border-radius: 30px;
|
||
font-size: 13px;
|
||
color: #1f3f60;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.tag-input-item .remove {
|
||
cursor: pointer;
|
||
color: #ef4444;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.tag-input-field {
|
||
border: none;
|
||
outline: none;
|
||
padding: 4px 8px;
|
||
flex: 1;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.btn-save {
|
||
background: #10b981;
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border-radius: 40px;
|
||
border: none;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.btn-save:hover {
|
||
background: #059669;
|
||
}
|
||
|
||
.btn-cancel {
|
||
background: #eef4fa;
|
||
color: #1f3f60;
|
||
padding: 12px 24px;
|
||
border-radius: 40px;
|
||
border: none;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-cancel:hover {
|
||
background: #dbeafe;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.resume-experience-item,
|
||
.resume-education-item {
|
||
background: #f9fcff;
|
||
border-radius: 20px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
position: relative;
|
||
}
|
||
|
||
.remove-item-btn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
background: #fee2e2;
|
||
border: none;
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 15px;
|
||
cursor: pointer;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.add-item-btn {
|
||
background: #eef4fa;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 30px;
|
||
cursor: pointer;
|
||
margin-top: 10px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.add-item-btn:hover {
|
||
background: #dbeafe;
|
||
}
|
||
</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('companies')">Компании</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="companiesTab" style="display: none;">
|
||
<div class="table-header">
|
||
<h2><i class="fas fa-building"></i> Управление компаниями</h2>
|
||
<div class="search-box">
|
||
<input type="text" id="companySearch" placeholder="Поиск по названию или email..." oninput="debounceSearch('companies')">
|
||
</div>
|
||
</div>
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Логотип</th>
|
||
<th>Название</th>
|
||
<th>Владелец</th>
|
||
<th>Email</th>
|
||
<th>Телефон</th>
|
||
<th>Сайт</th>
|
||
<th>Вакансий</th>
|
||
<th>Дата</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="companiesTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination" id="companiesPagination"></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()" style="padding: 12px 24px; background: #0b1c34; color: white; border: none; border-radius: 40px; cursor: pointer;">
|
||
<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="editVacancyModal">
|
||
<div class="modal-content" style="max-width: 700px;">
|
||
<button class="modal-close" onclick="closeEditVacancyModal()">×</button>
|
||
<h2>Редактирование вакансии</h2>
|
||
<input type="hidden" id="editVacancyId">
|
||
|
||
<div class="edit-form">
|
||
<div class="form-group">
|
||
<label>Название вакансии *</label>
|
||
<input type="text" id="editVacancyTitle" placeholder="Введите название">
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Зарплата</label>
|
||
<input type="text" id="editVacancySalary" placeholder="от 100 000 ₽">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Статус</label>
|
||
<select id="editVacancyStatus">
|
||
<option value="1">Активна</option>
|
||
<option value="0">Неактивна</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Теги</label>
|
||
<div class="tags-input" id="editVacancyTagsContainer">
|
||
<input type="text" class="tag-input-field" placeholder="Введите тег и нажмите Enter" id="editVacancyTagInput">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Описание вакансии</label>
|
||
<textarea id="editVacancyDescription" rows="6" placeholder="Подробное описание вакансии..."></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Контакт для связи</label>
|
||
<input type="text" id="editVacancyContact" placeholder="@telegram или email">
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-save" onclick="saveVacancyEdit()">💾 Сохранить</button>
|
||
<button class="btn-cancel" onclick="closeEditVacancyModal()">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно для редактирования резюме -->
|
||
<div class="modal" id="editResumeModal">
|
||
<div class="modal-content" style="max-width: 800px; max-height: 90vh; overflow-y: auto;">
|
||
<button class="modal-close" onclick="closeEditResumeModal()">×</button>
|
||
<h2>Редактирование резюме</h2>
|
||
<input type="hidden" id="editResumeId">
|
||
<input type="hidden" id="editResumeUserId">
|
||
|
||
<div class="edit-form">
|
||
<div class="form-group">
|
||
<label>Имя кандидата</label>
|
||
<input type="text" id="editResumeName" readonly style="background: #f5f5f5;">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Желаемая должность</label>
|
||
<input type="text" id="editResumePosition" placeholder="Например: Python разработчик">
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Желаемая зарплата</label>
|
||
<input type="text" id="editResumeSalary" placeholder="от 200 000 ₽">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>О себе</label>
|
||
<textarea id="editResumeAbout" rows="4" placeholder="Расскажите о себе..."></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Навыки (теги)</label>
|
||
<div class="tags-input" id="editResumeTagsContainer">
|
||
<input type="text" class="tag-input-field" placeholder="Введите тег и нажмите Enter" id="editResumeTagInput">
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin: 20px 0 15px;">Опыт работы</h3>
|
||
<div id="editResumeExperienceContainer"></div>
|
||
<button class="add-item-btn" onclick="addExperienceItem()">
|
||
<i class="fas fa-plus"></i> Добавить опыт работы
|
||
</button>
|
||
|
||
<h3 style="margin: 20px 0 15px;">Образование</h3>
|
||
<div id="editResumeEducationContainer"></div>
|
||
<button class="add-item-btn" onclick="addEducationItem()">
|
||
<i class="fas fa-plus"></i> Добавить образование
|
||
</button>
|
||
|
||
<div class="modal-actions" style="margin-top: 30px;">
|
||
<button class="btn-save" onclick="saveResumeEdit()">💾 Сохранить</button>
|
||
<button class="btn-cancel" onclick="closeEditResumeModal()">Отмена</button>
|
||
</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; background: #0b1c34; color: white; border: none; border-radius: 40px; cursor: pointer;">Создать</button>
|
||
<button class="btn-secondary" onclick="closeTagModal()" style="flex: 1; padding: 14px; background: #eef4fa; border: none; border-radius: 40px; cursor: pointer;">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно просмотра компании -->
|
||
<div class="modal" id="companyViewModal">
|
||
<div class="modal-content">
|
||
<button class="modal-close" onclick="closeCompanyModal()">×</button>
|
||
<h2 id="companyModalTitle">Информация о компании</h2>
|
||
<div id="companyModalContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно для просмотра резюме -->
|
||
<div class="modal" id="resumeViewModal">
|
||
<div class="modal-content" style="max-width: 700px; max-height: 80vh; overflow-y: auto;">
|
||
<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,
|
||
companies: 1
|
||
};
|
||
|
||
let searchTerms = {
|
||
users: '',
|
||
vacancies: '',
|
||
resumes: '',
|
||
companies: ''
|
||
};
|
||
|
||
let searchTimeouts = {};
|
||
|
||
// Переменные для редактирования
|
||
let editVacancyTags = [];
|
||
let editResumeTags = [];
|
||
let editResumeExperience = [];
|
||
let editResumeEducation = [];
|
||
|
||
// ========== УВЕДОМЛЕНИЯ ==========
|
||
function showNotification(message, type = 'success') {
|
||
const notification = document.getElementById('notification');
|
||
if (!notification) return;
|
||
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}"`);
|
||
|
||
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}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки пользователей');
|
||
|
||
const data = await response.json();
|
||
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 {
|
||
console.log(`📥 Загрузка вакансий: страница ${page}, поиск "${search}"`);
|
||
|
||
let url = `${API_BASE_URL}/admin/vacancies?page=${page}&limit=10`;
|
||
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 totalPages = data.total_pages || Math.ceil((data.total || vacancies.length) / 10) || 1;
|
||
|
||
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>
|
||
<div style="display: flex; gap: 5px;">
|
||
<button class="btn-icon edit" onclick="openEditVacancyModal(${v.id})" title="Редактировать">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn-icon view" onclick="window.open('/vacancy/${v.id}', '_blank')" 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>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
renderPagination('vacancies', totalPages, page);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка загрузки вакансий:', 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) throw new Error('Ошибка загрузки резюме');
|
||
|
||
const data = await response.json();
|
||
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>
|
||
<div style="display: flex; gap: 5px;">
|
||
<button class="btn-icon edit" onclick="openEditResumeModal(${r.id})" title="Редактировать">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<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>
|
||
</div>
|
||
</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 loadCompanies(page = 1, search = '') {
|
||
try {
|
||
console.log(`📥 Загрузка компаний: страница ${page}, поиск "${search}"`);
|
||
|
||
let url = `${API_BASE_URL}/admin/companies?page=${page}&limit=10`;
|
||
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 companies = data.companies || data;
|
||
const totalPages = data.total_pages || Math.ceil((data.total || companies.length) / 10) || 1;
|
||
|
||
const table = document.getElementById('companiesTable');
|
||
|
||
if (!companies || companies.length === 0) {
|
||
table.innerHTML = '<tr><td colspan="10" style="text-align: center;">Компании не найдены</td></tr>';
|
||
return;
|
||
}
|
||
|
||
table.innerHTML = companies.map(c => `
|
||
<tr>
|
||
<td>${c.id}</td>
|
||
<td>
|
||
<div class="company-logo">
|
||
${c.logo ?
|
||
`<img src="${escapeHtml(c.logo)}" alt="${escapeHtml(c.name)}" style="width: 100%; height: 100%; object-fit: cover;">` :
|
||
`<i class="fas fa-building"></i>`
|
||
}
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<a href="/company/${c.id}" target="_blank" style="color: #0b1c34; text-decoration: none; font-weight: 600;">
|
||
${escapeHtml(c.name)}
|
||
<i class="fas fa-external-link-alt" style="font-size: 12px; color: #4f7092;"></i>
|
||
</a>
|
||
</td>
|
||
<td>
|
||
<a href="/user/${c.user_id}" target="_blank" style="color: #3b82f6; text-decoration: none;">
|
||
${escapeHtml(c.owner_name || '—')}
|
||
</a>
|
||
</td>
|
||
<td>${escapeHtml(c.email || '—')}</td>
|
||
<td>${escapeHtml(c.phone || '—')}</td>
|
||
<td>${c.website ? `<a href="${escapeHtml(c.website)}" target="_blank" style="color: #3b82f6;">${escapeHtml(c.website)}</a>` : '—'}</td>
|
||
<td>${c.vacancies_count || 0}</td>
|
||
<td>${new Date(c.created_at).toLocaleDateString()}</td>
|
||
<td>
|
||
<div style="display: flex; gap: 5px;">
|
||
<button class="btn-icon view" onclick="viewCompany(${c.id})" title="Просмотр">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
<button class="btn-icon delete" onclick="deleteCompany(${c.id})" title="Удалить">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
renderPagination('companies', totalPages, page);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка загрузки компаний:', error);
|
||
document.getElementById('companiesTable').innerHTML = '<tr><td colspan="10" style="text-align: center;">Ошибка загрузки</td></tr>';
|
||
}
|
||
}
|
||
|
||
// ========== ПРОСМОТР РЕЗЮМЕ ==========
|
||
async function viewResume(resumeId) {
|
||
try {
|
||
console.log('📥 Загрузка деталей резюме ID:', resumeId);
|
||
|
||
const response = await fetch(`${API_BASE_URL}/resumes/${resumeId}`);
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки резюме');
|
||
|
||
const resume = await response.json();
|
||
console.log('✅ Данные резюме:', resume);
|
||
|
||
// Проверяем существование элемента
|
||
const modalNameElement = document.getElementById('resumeModalName');
|
||
const modalContentElement = document.getElementById('resumeModalContent');
|
||
|
||
if (!modalNameElement) {
|
||
console.error('❌ Элемент resumeModalName не найден в DOM');
|
||
showNotification('Ошибка: не найден элемент для отображения', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!modalContentElement) {
|
||
console.error('❌ Элемент resumeModalContent не найден в DOM');
|
||
showNotification('Ошибка: не найден элемент для содержимого', 'error');
|
||
return;
|
||
}
|
||
|
||
modalNameElement.textContent = escapeHtml(resume.full_name || 'Резюме');
|
||
|
||
// Формируем HTML для опыта работы
|
||
const experienceHtml = resume.work_experience && resume.work_experience.length > 0
|
||
? resume.work_experience.map(exp => `
|
||
<div class="exp-item" style="margin-bottom: 15px; padding: 15px; background: #f9fcff; border-radius: 15px;">
|
||
<div style="display: flex; justify-content: space-between; flex-wrap: wrap;">
|
||
<strong>${escapeHtml(exp.position)}</strong>
|
||
<span class="period" style="color: #4f7092;">${escapeHtml(exp.period || '')}</span>
|
||
</div>
|
||
<div style="color: #3b82f6;">${escapeHtml(exp.company)}</div>
|
||
${exp.description ? `<p style="margin-top: 8px; font-size: 13px; color: #1f3f60;">${escapeHtml(exp.description.substring(0, 150))}${exp.description.length > 150 ? '...' : ''}</p>` : ''}
|
||
</div>
|
||
`).join('')
|
||
: '<p style="color: #4f7092;">Опыт работы не указан</p>';
|
||
|
||
// Формируем HTML для образования
|
||
const educationHtml = resume.education && resume.education.length > 0
|
||
? resume.education.map(edu => `
|
||
<div class="edu-item" style="margin-bottom: 15px; padding: 15px; background: #f9fcff; border-radius: 15px;">
|
||
<div style="display: flex; justify-content: space-between; flex-wrap: wrap;">
|
||
<strong>${escapeHtml(edu.institution)}</strong>
|
||
<span class="year" style="color: #4f7092;">${escapeHtml(edu.graduation_year || '')}</span>
|
||
</div>
|
||
<div style="color: #3b82f6;">${escapeHtml(edu.specialty || '')}</div>
|
||
</div>
|
||
`).join('')
|
||
: '<p style="color: #4f7092;">Образование не указано</p>';
|
||
|
||
// Формируем HTML для тегов
|
||
const tagsHtml = resume.tags && resume.tags.length > 0
|
||
? resume.tags.map(t => `<span class="badge" style="background: #eef4fa; color: #1f3f60; margin: 2px; padding: 4px 8px; border-radius: 20px;">${escapeHtml(t.name || t)}</span>`).join('')
|
||
: '<span style="color: #4f7092;">—</span>';
|
||
|
||
modalContentElement.innerHTML = `
|
||
<div class="detail-section" style="margin-bottom: 25px;">
|
||
<h3 style="color: #1f3f60; margin-bottom: 10px;">Основная информация</h3>
|
||
<div class="detail-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Желаемая должность</div>
|
||
<div class="value" style="font-weight: 600;">${escapeHtml(resume.desired_position || '—')}</div>
|
||
</div>
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Зарплата</div>
|
||
<div class="value" style="font-weight: 600;">${escapeHtml(resume.desired_salary || '—')}</div>
|
||
</div>
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Просмотров</div>
|
||
<div class="value" style="font-weight: 600;">${resume.views || 0}</div>
|
||
</div>
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Обновлено</div>
|
||
<div class="value" style="font-weight: 600;">${resume.updated_at ? new Date(resume.updated_at).toLocaleDateString() : '—'}</div>
|
||
</div>
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Навыки</div>
|
||
<div class="value" style="font-weight: 600;">${tagsHtml}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section" style="margin-bottom: 25px;">
|
||
<h3 style="color: #1f3f60; margin-bottom: 10px;">О себе</h3>
|
||
<p style="background: #f9fcff; padding: 15px; border-radius: 15px; line-height: 1.6;">${escapeHtml(resume.about_me || 'Информация не заполнена')}</p>
|
||
</div>
|
||
|
||
<div class="detail-section" style="margin-bottom: 25px;">
|
||
<h3 style="color: #1f3f60; margin-bottom: 10px;">Опыт работы</h3>
|
||
<div class="experience-list">
|
||
${experienceHtml}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section" style="margin-bottom: 25px;">
|
||
<h3 style="color: #1f3f60; margin-bottom: 10px;">Образование</h3>
|
||
<div class="education-list">
|
||
${educationHtml}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section" style="margin-bottom: 25px;">
|
||
<h3 style="color: #1f3f60; margin-bottom: 10px;">Контакты</h3>
|
||
<div class="detail-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;">
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Email</div>
|
||
<div class="value" style="font-weight: 600;">${escapeHtml(resume.email || '—')}</div>
|
||
</div>
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Телефон</div>
|
||
<div class="value" style="font-weight: 600;">${escapeHtml(resume.phone || '—')}</div>
|
||
</div>
|
||
<div class="detail-item" style="padding: 10px; background: #f9fcff; border-radius: 15px;">
|
||
<div class="label" style="font-size: 12px; color: #4f7092;">Telegram</div>
|
||
<div class="value" style="font-weight: 600;">${escapeHtml(resume.telegram || '—')}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const modal = document.getElementById('resumeViewModal');
|
||
if (modal) {
|
||
modal.classList.add('active');
|
||
} else {
|
||
console.error('❌ Модальное окно resumeViewModal не найдено');
|
||
showNotification('Ошибка: модальное окно не найдено', 'error');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error loading resume details:', error);
|
||
showNotification('Ошибка загрузки деталей резюме', 'error');
|
||
}
|
||
}
|
||
|
||
function closeResumeModal() {
|
||
const modal = document.getElementById('resumeViewModal');
|
||
if (modal) {
|
||
modal.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
// ========== ПРОСМОТР КОМПАНИИ ==========
|
||
async function viewCompany(companyId) {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/companies/${companyId}`);
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки компании');
|
||
|
||
const company = await response.json();
|
||
|
||
document.getElementById('companyModalTitle').textContent = company.name;
|
||
|
||
const vacanciesResponse = await fetch(`${API_BASE_URL}/vacancies/all?company_id=${companyId}&limit=5`);
|
||
const vacanciesData = vacanciesResponse.ok ? await vacanciesResponse.json() : { vacancies: [] };
|
||
|
||
document.getElementById('companyModalContent').innerHTML = `
|
||
<div class="detail-section">
|
||
<h3>Основная информация</h3>
|
||
<div class="detail-grid">
|
||
<div class="detail-item">
|
||
<div class="label">Название</div>
|
||
<div class="value">${escapeHtml(company.name)}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Владелец</div>
|
||
<div class="value">
|
||
<a href="/user/${company.user_id}" target="_blank">${escapeHtml(company.owner_name || 'Пользователь')}</a>
|
||
</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Дата регистрации</div>
|
||
<div class="value">${new Date(company.created_at).toLocaleDateString()}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Активных вакансий</div>
|
||
<div class="value">${vacanciesData.vacancies?.filter(v => v.is_active).length || 0}</div>
|
||
</div>
|
||
</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(company.email || '—')}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Телефон</div>
|
||
<div class="value">${escapeHtml(company.phone || '—')}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Адрес</div>
|
||
<div class="value">${escapeHtml(company.address || '—')}</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<div class="label">Сайт</div>
|
||
<div class="value">${company.website ? `<a href="${escapeHtml(company.website)}" target="_blank">${escapeHtml(company.website)}</a>` : '—'}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${company.description ? `
|
||
<div class="detail-section">
|
||
<h3>Описание компании</h3>
|
||
<div style="background: #f9fcff; padding: 15px; border-radius: 15px; line-height: 1.6;">
|
||
${escapeHtml(company.description).replace(/\n/g, '<br>')}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="detail-section">
|
||
<h3>Последние вакансии (${vacanciesData.vacancies?.length || 0})</h3>
|
||
${vacanciesData.vacancies && vacanciesData.vacancies.length > 0 ?
|
||
vacanciesData.vacancies.slice(0, 5).map(v => `
|
||
<div style="padding: 10px; background: #f9fcff; border-radius: 10px; margin-bottom: 10px;">
|
||
<a href="/vacancy/${v.id}" target="_blank" style="color: #0b1c34; font-weight: 600;">${escapeHtml(v.title)}</a>
|
||
<span style="color: #4f7092; font-size: 12px; margin-left: 10px;">${escapeHtml(v.salary || 'з/п не указана')}</span>
|
||
<span class="badge ${v.is_active ? 'active' : 'inactive'}" style="margin-left: 10px;">${v.is_active ? 'Активна' : 'Неактивна'}</span>
|
||
</div>
|
||
`).join('') :
|
||
'<p style="color: #4f7092;">Нет вакансий</p>'
|
||
}
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('companyViewModal').classList.add('active');
|
||
|
||
} catch (error) {
|
||
console.error('Error loading company details:', error);
|
||
showNotification('Ошибка загрузки деталей компании', 'error');
|
||
}
|
||
}
|
||
|
||
function closeCompanyModal() {
|
||
document.getElementById('companyViewModal').classList.remove('active');
|
||
}
|
||
|
||
// ========== УДАЛЕНИЕ ==========
|
||
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');
|
||
}
|
||
}
|
||
|
||
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 deleteCompany(companyId) {
|
||
if (!confirm('Вы уверены, что хотите удалить эту компанию? Это действие нельзя отменить. Все вакансии компании также будут удалены.')) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/admin/companies/${companyId}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка удаления');
|
||
|
||
showNotification('Компания удалена', 'success');
|
||
loadCompanies(currentPage.companies, searchTerms.companies);
|
||
loadStats();
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting company:', error);
|
||
showNotification('Ошибка при удалении компании', 'error');
|
||
}
|
||
}
|
||
|
||
// ========== РЕДАКТИРОВАНИЕ ВАКАНСИЙ ==========
|
||
|
||
async function openEditVacancyModal(vacancyId) {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`);
|
||
if (!response.ok) throw new Error('Ошибка загрузки');
|
||
|
||
const vacancy = await response.json();
|
||
|
||
document.getElementById('editVacancyId').value = vacancy.id;
|
||
document.getElementById('editVacancyTitle').value = vacancy.title || '';
|
||
document.getElementById('editVacancySalary').value = vacancy.salary || '';
|
||
document.getElementById('editVacancyStatus').value = vacancy.is_active ? '1' : '0';
|
||
document.getElementById('editVacancyDescription').value = vacancy.description || '';
|
||
document.getElementById('editVacancyContact').value = vacancy.contact || '';
|
||
|
||
editVacancyTags = (vacancy.tags || []).map(t => t.name);
|
||
renderVacancyTags();
|
||
|
||
document.getElementById('editVacancyModal').classList.add('active');
|
||
|
||
} catch (error) {
|
||
console.error('Error loading vacancy for edit:', error);
|
||
showNotification('Ошибка загрузки данных вакансии', 'error');
|
||
}
|
||
}
|
||
|
||
function closeEditVacancyModal() {
|
||
document.getElementById('editVacancyModal').classList.remove('active');
|
||
editVacancyTags = [];
|
||
}
|
||
|
||
function renderVacancyTags() {
|
||
const container = document.getElementById('editVacancyTagsContainer');
|
||
const input = document.getElementById('editVacancyTagInput');
|
||
|
||
container.innerHTML = '';
|
||
|
||
editVacancyTags.forEach(tag => {
|
||
const tagSpan = document.createElement('span');
|
||
tagSpan.className = 'tag-input-item';
|
||
tagSpan.innerHTML = `${escapeHtml(tag)} <span class="remove" onclick="removeVacancyTag('${escapeHtml(tag)}')">×</span>`;
|
||
container.appendChild(tagSpan);
|
||
});
|
||
|
||
container.appendChild(input);
|
||
input.value = '';
|
||
input.focus();
|
||
}
|
||
|
||
function addVacancyTag() {
|
||
const input = document.getElementById('editVacancyTagInput');
|
||
const tag = input.value.trim();
|
||
|
||
if (tag && !editVacancyTags.includes(tag)) {
|
||
editVacancyTags.push(tag);
|
||
renderVacancyTags();
|
||
}
|
||
}
|
||
|
||
function removeVacancyTag(tag) {
|
||
editVacancyTags = editVacancyTags.filter(t => t !== tag);
|
||
renderVacancyTags();
|
||
}
|
||
|
||
async function saveVacancyEdit() {
|
||
const vacancyId = document.getElementById('editVacancyId').value;
|
||
const data = {
|
||
title: document.getElementById('editVacancyTitle').value,
|
||
salary: document.getElementById('editVacancySalary').value || null,
|
||
description: document.getElementById('editVacancyDescription').value || null,
|
||
contact: document.getElementById('editVacancyContact').value || null,
|
||
tags: editVacancyTags,
|
||
is_active: document.getElementById('editVacancyStatus').value === '1'
|
||
};
|
||
|
||
if (!data.title) {
|
||
showNotification('Введите название вакансии', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка сохранения');
|
||
|
||
showNotification('Вакансия обновлена', 'success');
|
||
closeEditVacancyModal();
|
||
loadVacancies(currentPage.vacancies, searchTerms.vacancies);
|
||
|
||
} catch (error) {
|
||
console.error('Error saving vacancy:', error);
|
||
showNotification(error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ========== РЕДАКТИРОВАНИЕ РЕЗЮМЕ ==========
|
||
|
||
async function openEditResumeModal(resumeId) {
|
||
try {
|
||
console.log('📥 Загрузка резюме для редактирования ID:', resumeId);
|
||
|
||
// Используем админский эндпоинт для получения данных
|
||
const response = await fetch(`${API_BASE_URL}/admin/resumes/${resumeId}`, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка загрузки');
|
||
|
||
const resume = await response.json();
|
||
console.log('✅ Данные резюме:', resume);
|
||
|
||
document.getElementById('editResumeId').value = resume.id;
|
||
document.getElementById('editResumeUserId').value = resume.user_id;
|
||
document.getElementById('editResumeName').value = resume.full_name || '';
|
||
document.getElementById('editResumePosition').value = resume.desired_position || '';
|
||
document.getElementById('editResumeSalary').value = resume.desired_salary || '';
|
||
document.getElementById('editResumeAbout').value = resume.about_me || '';
|
||
|
||
// Загружаем теги
|
||
editResumeTags = (resume.tags || []).map(t => t.name || t);
|
||
console.log('📌 Теги:', editResumeTags);
|
||
renderResumeTags();
|
||
|
||
// Загружаем опыт работы
|
||
editResumeExperience = (resume.work_experience || []).map(exp => ({
|
||
position: exp.position || '',
|
||
company: exp.company || '',
|
||
period: exp.period || '',
|
||
description: exp.description || ''
|
||
}));
|
||
renderResumeExperience();
|
||
|
||
// Загружаем образование
|
||
editResumeEducation = (resume.education || []).map(edu => ({
|
||
institution: edu.institution || '',
|
||
specialty: edu.specialty || '',
|
||
graduation_year: edu.graduation_year || ''
|
||
}));
|
||
renderResumeEducation();
|
||
|
||
document.getElementById('editResumeModal').classList.add('active');
|
||
|
||
} catch (error) {
|
||
console.error('Error loading resume for edit:', error);
|
||
showNotification('Ошибка загрузки данных резюме', 'error');
|
||
}
|
||
}
|
||
|
||
function closeEditResumeModal() {
|
||
document.getElementById('editResumeModal').classList.remove('active');
|
||
editResumeTags = [];
|
||
editResumeExperience = [];
|
||
editResumeEducation = [];
|
||
}
|
||
|
||
function renderResumeTags() {
|
||
const container = document.getElementById('editResumeTagsContainer');
|
||
if (!container) return;
|
||
|
||
// Сохраняем ссылку на input, если он есть
|
||
let existingInput = document.getElementById('editResumeTagInput');
|
||
|
||
// Очищаем контейнер
|
||
container.innerHTML = '';
|
||
|
||
// Добавляем теги
|
||
if (editResumeTags && editResumeTags.length > 0) {
|
||
editResumeTags.forEach(tag => {
|
||
if (tag && tag.trim()) {
|
||
const tagSpan = document.createElement('span');
|
||
tagSpan.className = 'tag-input-item';
|
||
tagSpan.innerHTML = `${escapeHtml(tag)} <span class="remove" onclick="removeResumeTag('${escapeHtml(tag)}')">×</span>`;
|
||
container.appendChild(tagSpan);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Создаем input для ввода новых тегов
|
||
const input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.className = 'tag-input-field';
|
||
input.id = 'editResumeTagInput';
|
||
input.placeholder = 'Введите тег и нажмите Enter';
|
||
input.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addResumeTag();
|
||
}
|
||
});
|
||
|
||
container.appendChild(input);
|
||
}
|
||
|
||
function addResumeTag() {
|
||
const input = document.getElementById('editResumeTagInput');
|
||
if (!input) return;
|
||
|
||
const tag = input.value.trim();
|
||
if (tag && !editResumeTags.includes(tag)) {
|
||
editResumeTags.push(tag);
|
||
renderResumeTags();
|
||
}
|
||
input.value = '';
|
||
input.focus();
|
||
}
|
||
|
||
function removeResumeTag(tag) {
|
||
editResumeTags = editResumeTags.filter(t => t !== tag);
|
||
renderResumeTags();
|
||
}
|
||
|
||
function renderResumeExperience() {
|
||
const container = document.getElementById('editResumeExperienceContainer');
|
||
container.innerHTML = '';
|
||
|
||
editResumeExperience.forEach((exp, index) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'resume-experience-item';
|
||
div.innerHTML = `
|
||
<button class="remove-item-btn" onclick="removeExperienceItem(${index})">×</button>
|
||
<div class="form-group">
|
||
<label>Должность</label>
|
||
<input type="text" class="exp-position" value="${escapeHtml(exp.position)}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Компания</label>
|
||
<input type="text" class="exp-company" value="${escapeHtml(exp.company)}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Период</label>
|
||
<input type="text" class="exp-period" value="${escapeHtml(exp.period)}" placeholder="2022-2024">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Описание обязанностей</label>
|
||
<textarea class="exp-description" rows="3">${escapeHtml(exp.description)}</textarea>
|
||
</div>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.exp-position').forEach((input, idx) => {
|
||
input.addEventListener('change', () => updateExperienceItem(idx, 'position', input.value));
|
||
});
|
||
document.querySelectorAll('.exp-company').forEach((input, idx) => {
|
||
input.addEventListener('change', () => updateExperienceItem(idx, 'company', input.value));
|
||
});
|
||
document.querySelectorAll('.exp-period').forEach((input, idx) => {
|
||
input.addEventListener('change', () => updateExperienceItem(idx, 'period', input.value));
|
||
});
|
||
document.querySelectorAll('.exp-description').forEach((input, idx) => {
|
||
input.addEventListener('change', () => updateExperienceItem(idx, 'description', input.value));
|
||
});
|
||
}, 100);
|
||
}
|
||
|
||
function updateExperienceItem(index, field, value) {
|
||
if (editResumeExperience[index]) {
|
||
editResumeExperience[index][field] = value;
|
||
}
|
||
}
|
||
|
||
function addExperienceItem() {
|
||
editResumeExperience.push({
|
||
position: '',
|
||
company: '',
|
||
period: '',
|
||
description: ''
|
||
});
|
||
renderResumeExperience();
|
||
}
|
||
|
||
function removeExperienceItem(index) {
|
||
editResumeExperience.splice(index, 1);
|
||
renderResumeExperience();
|
||
}
|
||
|
||
function renderResumeEducation() {
|
||
const container = document.getElementById('editResumeEducationContainer');
|
||
container.innerHTML = '';
|
||
|
||
editResumeEducation.forEach((edu, index) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'resume-education-item';
|
||
div.innerHTML = `
|
||
<button class="remove-item-btn" onclick="removeEducationItem(${index})">×</button>
|
||
<div class="form-group">
|
||
<label>Учебное заведение</label>
|
||
<input type="text" class="edu-institution" value="${escapeHtml(edu.institution)}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Специальность</label>
|
||
<input type="text" class="edu-specialty" value="${escapeHtml(edu.specialty)}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Год окончания</label>
|
||
<input type="text" class="edu-year" value="${escapeHtml(edu.graduation_year)}">
|
||
</div>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.edu-institution').forEach((input, idx) => {
|
||
input.addEventListener('change', () => updateEducationItem(idx, 'institution', input.value));
|
||
});
|
||
document.querySelectorAll('.edu-specialty').forEach((input, idx) => {
|
||
input.addEventListener('change', () => updateEducationItem(idx, 'specialty', input.value));
|
||
});
|
||
document.querySelectorAll('.edu-year').forEach((input, idx) => {
|
||
input.addEventListener('change', () => updateEducationItem(idx, 'graduation_year', input.value));
|
||
});
|
||
}, 100);
|
||
}
|
||
|
||
function updateEducationItem(index, field, value) {
|
||
if (editResumeEducation[index]) {
|
||
editResumeEducation[index][field] = value;
|
||
}
|
||
}
|
||
|
||
function addEducationItem() {
|
||
editResumeEducation.push({
|
||
institution: '',
|
||
specialty: '',
|
||
graduation_year: ''
|
||
});
|
||
renderResumeEducation();
|
||
}
|
||
|
||
function removeEducationItem(index) {
|
||
editResumeEducation.splice(index, 1);
|
||
renderResumeEducation();
|
||
}
|
||
|
||
async function saveResumeEdit() {
|
||
const resumeId = document.getElementById('editResumeId').value;
|
||
|
||
const data = {
|
||
desired_position: document.getElementById('editResumePosition').value || null,
|
||
about_me: document.getElementById('editResumeAbout').value || null,
|
||
desired_salary: document.getElementById('editResumeSalary').value || null,
|
||
tags: editResumeTags,
|
||
work_experience: editResumeExperience,
|
||
education: editResumeEducation
|
||
};
|
||
|
||
console.log('📤 Отправка данных резюме:', JSON.stringify(data, null, 2));
|
||
|
||
try {
|
||
// Используем админский эндпоинт для обновления резюме
|
||
const response = await fetch(`${API_BASE_URL}/admin/resumes/${resumeId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
console.log('📥 Статус ответа:', response.status);
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
console.error('❌ Ошибка:', error);
|
||
throw new Error(error.detail || 'Ошибка сохранения');
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('✅ Результат сохранения:', result);
|
||
|
||
showNotification('Резюме обновлено', 'success');
|
||
closeEditResumeModal();
|
||
loadResumes(currentPage.resumes, searchTerms.resumes);
|
||
|
||
} catch (error) {
|
||
console.error('Error saving resume:', error);
|
||
showNotification(error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ========== ПАГИНАЦИЯ ==========
|
||
function renderPagination(type, totalPages, currentPageNum) {
|
||
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 === currentPageNum ? '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;
|
||
case 'companies':
|
||
loadCompanies(page, searchTerms.companies);
|
||
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;
|
||
case 'companies':
|
||
loadCompanies(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 tabMap = { 'users': 0, 'vacancies': 1, 'resumes': 2, 'companies': 3, 'tags': 4 };
|
||
const index = tabMap[tab];
|
||
if (index !== undefined) {
|
||
document.querySelectorAll('.tab')[index]?.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 === 'companies') {
|
||
document.getElementById('companiesTab').style.display = 'block';
|
||
loadCompanies(currentPage.companies, searchTerms.companies);
|
||
} else if (tab === 'tags') {
|
||
document.getElementById('tagsTab').style.display = 'block';
|
||
loadPopularTags();
|
||
}
|
||
}
|
||
|
||
// ========== ФИЛЬТРАЦИЯ ПО СТАТИСТИКЕ ==========
|
||
function filterByType(type) {
|
||
if (type === 'users' || type === 'employees' || type === 'employers') {
|
||
switchTab('users');
|
||
} else if (type === 'vacancies') {
|
||
switchTab('vacancies');
|
||
} else if (type === 'resumes') {
|
||
switchTab('resumes');
|
||
} else if (type === 'applications') {
|
||
window.location.href = '/applications';
|
||
}
|
||
}
|
||
|
||
// ========== РАБОТА С ТЕГАМИ ==========
|
||
async function loadPopularTags() {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/tags`);
|
||
const tags = await response.json();
|
||
|
||
const tagsCloud = document.getElementById('tagsCloud');
|
||
tagsCloud.innerHTML = tags.slice(0, 20).map(t => `
|
||
<div class="tag-item">
|
||
${escapeHtml(t.name)}
|
||
<span class="count">0</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
const popularTagsTable = document.getElementById('popularTagsTable');
|
||
popularTagsTable.innerHTML = tags.slice(0, 10).map(t => `
|
||
<tr>
|
||
<td><strong>${escapeHtml(t.name)}</strong></td>
|
||
<td>${escapeHtml(t.category || '—')}很少
|
||
<td>0很少
|
||
<td>0很少
|
||
<td>0很少
|
||
</tr>
|
||
`).join('');
|
||
|
||
} catch (error) {
|
||
console.error('Error loading tags:', error);
|
||
}
|
||
}
|
||
|
||
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();
|
||
loadPopularTags();
|
||
|
||
} catch (error) {
|
||
console.error('Error creating tag:', error);
|
||
showNotification(error.message, '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]];
|
||
}
|
||
|
||
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 = '/';
|
||
}
|
||
|
||
// ========== ИНИЦИАЛИЗАЦИЯ СОБЫТИЙ ==========
|
||
function initTagInputs() {
|
||
const vacancyTagInput = document.getElementById('editVacancyTagInput');
|
||
if (vacancyTagInput) {
|
||
vacancyTagInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addVacancyTag();
|
||
}
|
||
});
|
||
}
|
||
|
||
const resumeTagInput = document.getElementById('editResumeTagInput');
|
||
if (resumeTagInput) {
|
||
resumeTagInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addResumeTag();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// ========== ЗАКРЫТИЕ МОДАЛОК ПО КЛИКУ ВНЕ ==========
|
||
window.onclick = function(event) {
|
||
const tagModal = document.getElementById('tagModal');
|
||
const resumeModal = document.getElementById('resumeViewModal');
|
||
const companyModal = document.getElementById('companyViewModal');
|
||
const editVacancyModal = document.getElementById('editVacancyModal');
|
||
const editResumeModal = document.getElementById('editResumeModal');
|
||
|
||
if (event.target === tagModal) tagModal.classList.remove('active');
|
||
if (event.target === resumeModal) resumeModal.classList.remove('active');
|
||
if (event.target === companyModal) companyModal.classList.remove('active');
|
||
if (event.target === editVacancyModal) editVacancyModal.classList.remove('active');
|
||
if (event.target === editResumeModal) editResumeModal.classList.remove('active');
|
||
};
|
||
|
||
// ========== ИНИЦИАЛИЗАЦИЯ ==========
|
||
window.addEventListener('load', () => {
|
||
updateNavigation();
|
||
initTagInputs();
|
||
loadStats();
|
||
loadUsers();
|
||
loadCompanies();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |