Files
yarmarka/templates/admin.html
2026-03-26 19:00:41 +03:00

2333 lines
96 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель | Rabota.Today</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
background: linear-gradient(145deg, #eef5fa 0%, #e0eaf5 100%);
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #0b1c34;
color: white;
padding: 20px 40px;
border-radius: 40px;
margin-bottom: 40px;
display: flex;
justify-content: space-between;
align-items: center;
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()">&times;</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()">&times;</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()">&times;</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()">&times;</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()">&times;</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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// ========== НАВИГАЦИЯ ==========
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>