admin company page
This commit is contained in:
119
server.py
119
server.py
@@ -2161,6 +2161,125 @@ async def create_or_update_company(
|
|||||||
return dict(cursor.fetchone())
|
return dict(cursor.fetchone())
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/admin/companies")
|
||||||
|
async def get_all_companies_admin(
|
||||||
|
user_id: int = Depends(get_current_user),
|
||||||
|
page: int = 1,
|
||||||
|
limit: int = 10,
|
||||||
|
search: str = None
|
||||||
|
):
|
||||||
|
"""Получение всех компаний для админки"""
|
||||||
|
try:
|
||||||
|
print(f"👑 Админ {user_id} запрашивает список компаний")
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Проверка прав администратора
|
||||||
|
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
if not user or not user["is_admin"]:
|
||||||
|
raise HTTPException(status_code=403, detail="Доступ запрещен")
|
||||||
|
|
||||||
|
# Базовый запрос
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
c.*,
|
||||||
|
u.full_name as owner_name,
|
||||||
|
(SELECT COUNT(*) FROM vacancies WHERE user_id = c.user_id AND is_active = 1) as vacancies_count
|
||||||
|
FROM companies c
|
||||||
|
JOIN users u ON c.user_id = u.id
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
# Поиск
|
||||||
|
if search:
|
||||||
|
query += " AND (c.name LIKE ? OR c.email LIKE ? OR u.full_name LIKE ?)"
|
||||||
|
search_term = f"%{search}%"
|
||||||
|
params.extend([search_term, search_term, search_term])
|
||||||
|
|
||||||
|
# Сортировка
|
||||||
|
query += " ORDER BY c.created_at DESC"
|
||||||
|
|
||||||
|
# Пагинация
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
query += " LIMIT ? OFFSET ?"
|
||||||
|
params.extend([limit, offset])
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
companies = cursor.fetchall()
|
||||||
|
|
||||||
|
# Получаем общее количество
|
||||||
|
count_query = "SELECT COUNT(*) FROM companies"
|
||||||
|
if search:
|
||||||
|
count_query += " WHERE name LIKE ? OR email LIKE ?"
|
||||||
|
cursor.execute(count_query, [search_term, search_term])
|
||||||
|
else:
|
||||||
|
cursor.execute(count_query)
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
result = [dict(c) for c in companies]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"companies": result,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"total_pages": (total + limit - 1) // limit,
|
||||||
|
"limit": limit
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при загрузке компаний: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/admin/companies/{company_id}")
|
||||||
|
async def delete_company_admin(
|
||||||
|
company_id: int,
|
||||||
|
user_id: int = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Удаление компании (только для админа)"""
|
||||||
|
try:
|
||||||
|
print(f"👑 Админ {user_id} пытается удалить компанию {company_id}")
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Проверка прав администратора
|
||||||
|
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
if not user or not user["is_admin"]:
|
||||||
|
raise HTTPException(status_code=403, detail="Доступ запрещен")
|
||||||
|
|
||||||
|
# Проверяем существование компании
|
||||||
|
cursor.execute("SELECT user_id FROM companies WHERE id = ?", (company_id,))
|
||||||
|
company = cursor.fetchone()
|
||||||
|
if not company:
|
||||||
|
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||||||
|
|
||||||
|
# Удаляем вакансии компании
|
||||||
|
cursor.execute("DELETE FROM vacancies WHERE user_id = ?", (company["user_id"],))
|
||||||
|
|
||||||
|
# Удаляем компанию
|
||||||
|
cursor.execute("DELETE FROM companies WHERE id = ?", (company_id,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"✅ Компания {company_id} успешно удалена")
|
||||||
|
|
||||||
|
return {"message": "Компания успешно удалена"}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при удалении компании {company_id}: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/companies/{company_id}")
|
@app.get("/api/companies/{company_id}")
|
||||||
async def get_company_by_id(company_id: int):
|
async def get_company_by_id(company_id: int):
|
||||||
"""Получение информации о компании по ID (публичный эндпоинт)"""
|
"""Получение информации о компании по ID (публичный эндпоинт)"""
|
||||||
|
|||||||
@@ -284,29 +284,23 @@
|
|||||||
background: #bae6fd;
|
background: #bae6fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-cloud {
|
.company-logo {
|
||||||
display: flex;
|
width: 40px;
|
||||||
flex-wrap: wrap;
|
height: 40px;
|
||||||
gap: 10px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-item {
|
|
||||||
background: #eef4fa;
|
background: #eef4fa;
|
||||||
padding: 8px 16px;
|
border-radius: 10px;
|
||||||
border-radius: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: center;
|
||||||
font-size: 14px;
|
font-size: 20px;
|
||||||
|
color: #3b82f6;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-item .count {
|
.company-logo img {
|
||||||
background: #3b82f6;
|
width: 100%;
|
||||||
color: white;
|
height: 100%;
|
||||||
padding: 2px 8px;
|
object-fit: cover;
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
@@ -402,26 +396,6 @@
|
|||||||
color: #0b1c34;
|
color: #0b1c34;
|
||||||
}
|
}
|
||||||
|
|
||||||
.experience-list, .education-list {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exp-item, .edu-item {
|
|
||||||
background: #f9fcff;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exp-item strong, .edu-item strong {
|
|
||||||
color: #0b1c34;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exp-item .period, .edu-item .year {
|
|
||||||
color: #4f7092;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
@@ -488,90 +462,6 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для ссылок на профили */
|
|
||||||
.user-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: #0b1c34;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 30px;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-link:hover {
|
|
||||||
background: #eef4fa;
|
|
||||||
color: #0b1c34;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-link i {
|
|
||||||
color: #3b82f6;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-link .external-icon {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4f7092;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-link:hover .external-icon {
|
|
||||||
opacity: 1;
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для кнопок действий */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.2s;
|
|
||||||
background: transparent;
|
|
||||||
color: #4f7092;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon:hover {
|
|
||||||
background: #eef4fa;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon.view:hover {
|
|
||||||
background: #dbeafe;
|
|
||||||
color: #1e40af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon.delete:hover {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Email ссылка */
|
|
||||||
.email-link {
|
|
||||||
color: #3b82f6;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -619,6 +509,7 @@
|
|||||||
<button class="tab active" onclick="switchTab('users')">Пользователи</button>
|
<button class="tab active" onclick="switchTab('users')">Пользователи</button>
|
||||||
<button class="tab" onclick="switchTab('vacancies')">Вакансии</button>
|
<button class="tab" onclick="switchTab('vacancies')">Вакансии</button>
|
||||||
<button class="tab" onclick="switchTab('resumes')">Резюме</button>
|
<button class="tab" onclick="switchTab('resumes')">Резюме</button>
|
||||||
|
<button class="tab" onclick="switchTab('companies')">Компании</button>
|
||||||
<button class="tab" onclick="switchTab('tags')">Теги</button>
|
<button class="tab" onclick="switchTab('tags')">Теги</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -709,11 +600,41 @@
|
|||||||
<div class="pagination" id="resumesPagination"></div>
|
<div class="pagination" id="resumesPagination"></div>
|
||||||
</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 id="tagsTab" style="display: none;">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<h2><i class="fas fa-tags"></i> Управление тегами</h2>
|
<h2><i class="fas fa-tags"></i> Управление тегами</h2>
|
||||||
<button class="btn-primary" onclick="showAddTagModal()">
|
<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> Добавить тег
|
<i class="fas fa-plus"></i> Добавить тег
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -761,19 +682,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; margin-top: 30px;">
|
<div style="display: flex; gap: 10px; margin-top: 30px;">
|
||||||
<button class="btn-primary" onclick="createTag()" style="flex: 1; padding: 14px;">Создать</button>
|
<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;">Отмена</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Модальное окно просмотра резюме -->
|
<!-- Модальное окно просмотра компании -->
|
||||||
<div class="modal" id="resumeViewModal">
|
<div class="modal" id="companyViewModal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<button class="modal-close" onclick="closeResumeModal()">×</button>
|
<button class="modal-close" onclick="closeCompanyModal()">×</button>
|
||||||
<h2 id="resumeModalName"></h2>
|
<h2 id="companyModalTitle">Информация о компании</h2>
|
||||||
|
<div id="companyModalContent"></div>
|
||||||
<div id="resumeModalContent"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -793,13 +713,15 @@
|
|||||||
let currentPage = {
|
let currentPage = {
|
||||||
users: 1,
|
users: 1,
|
||||||
vacancies: 1,
|
vacancies: 1,
|
||||||
resumes: 1
|
resumes: 1,
|
||||||
|
companies: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
let searchTerms = {
|
let searchTerms = {
|
||||||
users: '',
|
users: '',
|
||||||
vacancies: '',
|
vacancies: '',
|
||||||
resumes: ''
|
resumes: '',
|
||||||
|
companies: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
let searchTimeouts = {};
|
let searchTimeouts = {};
|
||||||
@@ -807,6 +729,7 @@
|
|||||||
// ========== УВЕДОМЛЕНИЯ ==========
|
// ========== УВЕДОМЛЕНИЯ ==========
|
||||||
function showNotification(message, type = 'success') {
|
function showNotification(message, type = 'success') {
|
||||||
const notification = document.getElementById('notification');
|
const notification = document.getElementById('notification');
|
||||||
|
if (!notification) return;
|
||||||
notification.className = `notification ${type}`;
|
notification.className = `notification ${type}`;
|
||||||
notification.innerHTML = message;
|
notification.innerHTML = message;
|
||||||
notification.style.display = 'block';
|
notification.style.display = 'block';
|
||||||
@@ -860,26 +783,17 @@
|
|||||||
async function loadUsers(page = 1, search = '') {
|
async function loadUsers(page = 1, search = '') {
|
||||||
try {
|
try {
|
||||||
console.log(`📥 Загрузка пользователей: страница ${page}, поиск "${search}"`);
|
console.log(`📥 Загрузка пользователей: страница ${page}, поиск "${search}"`);
|
||||||
console.log('🔑 Токен:', token ? 'Есть' : 'Нет');
|
|
||||||
|
|
||||||
let url = `${API_BASE_URL}/admin/users?page=${page}&limit=10`;
|
let url = `${API_BASE_URL}/admin/users?page=${page}&limit=10`;
|
||||||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Ошибка загрузки пользователей');
|
||||||
console.error('❌ Ошибка ответа:', response.status, response.statusText);
|
|
||||||
throw new Error('Ошибка загрузки пользователей');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('✅ Данные пользователей:', data);
|
|
||||||
|
|
||||||
const users = data.users || data;
|
const users = data.users || data;
|
||||||
const totalPages = data.total_pages || Math.ceil((data.total || users.length) / 10) || 1;
|
const totalPages = data.total_pages || Math.ceil((data.total || users.length) / 10) || 1;
|
||||||
|
|
||||||
@@ -940,7 +854,9 @@
|
|||||||
// ========== ЗАГРУЗКА ВАКАНСИЙ ==========
|
// ========== ЗАГРУЗКА ВАКАНСИЙ ==========
|
||||||
async function loadVacancies(page = 1, search = '') {
|
async function loadVacancies(page = 1, search = '') {
|
||||||
try {
|
try {
|
||||||
let url = `${API_BASE_URL}/admin/vacancies?page=${page}`;
|
console.log(`📥 Загрузка вакансий: страница ${page}, поиск "${search}"`);
|
||||||
|
|
||||||
|
let url = `${API_BASE_URL}/admin/vacancies?page=${page}&limit=10`;
|
||||||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -951,6 +867,7 @@
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const vacancies = data.vacancies || data;
|
const vacancies = data.vacancies || data;
|
||||||
|
const totalPages = data.total_pages || Math.ceil((data.total || vacancies.length) / 10) || 1;
|
||||||
|
|
||||||
const table = document.getElementById('vacanciesTable');
|
const table = document.getElementById('vacanciesTable');
|
||||||
|
|
||||||
@@ -978,20 +895,22 @@
|
|||||||
<td>${v.views || 0}</td>
|
<td>${v.views || 0}</td>
|
||||||
<td>${new Date(v.created_at).toLocaleDateString()}</td>
|
<td>${new Date(v.created_at).toLocaleDateString()}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn-icon view" onclick="viewVacancy(${v.id})" title="Просмотр">
|
<div style="display: flex; gap: 5px;">
|
||||||
|
<button class="btn-icon view" onclick="window.open('/vacancy/${v.id}', '_blank')" title="Просмотр">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon delete" onclick="deleteVacancy(${v.id})" title="Удалить">
|
<button class="btn-icon delete" onclick="deleteVacancy(${v.id})" title="Удалить">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
renderPagination('vacancies', data.total_pages || 1, page);
|
renderPagination('vacancies', totalPages, page);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading vacancies:', error);
|
console.error('❌ Ошибка загрузки вакансий:', error);
|
||||||
document.getElementById('vacanciesTable').innerHTML = '<tr><td colspan="9" style="text-align: center;">Ошибка загрузки</td></tr>';
|
document.getElementById('vacanciesTable').innerHTML = '<tr><td colspan="9" style="text-align: center;">Ошибка загрузки</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1008,16 +927,9 @@
|
|||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Ошибка загрузки резюме');
|
||||||
console.warn('Эндпоинт admin/resumes не найден, пробуем резервный вариант');
|
|
||||||
// Пробуем загрузить через публичный эндпоинт
|
|
||||||
await loadResumesFallback(page, search);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('✅ Резюме загружены:', data);
|
|
||||||
|
|
||||||
const resumes = data.resumes || data;
|
const resumes = data.resumes || data;
|
||||||
const totalPages = data.total_pages || Math.ceil((data.total || resumes.length) / 10) || 1;
|
const totalPages = data.total_pages || Math.ceil((data.total || resumes.length) / 10) || 1;
|
||||||
|
|
||||||
@@ -1029,9 +941,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.innerHTML = resumes.map(r => {
|
table.innerHTML = resumes.map(r => {
|
||||||
// Безопасное получение опыта
|
|
||||||
const expCount = r.work_experience ? r.work_experience.length : (r.experience_count || 0);
|
const expCount = r.work_experience ? r.work_experience.length : (r.experience_count || 0);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${r.id}</td>
|
<td>${r.id}</td>
|
||||||
@@ -1047,12 +957,14 @@
|
|||||||
<td>${r.views || 0}</td>
|
<td>${r.views || 0}</td>
|
||||||
<td>${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'}</td>
|
<td>${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div style="display: flex; gap: 5px;">
|
||||||
<button class="btn-icon view" onclick="viewResume(${r.id})" title="Просмотр">
|
<button class="btn-icon view" onclick="viewResume(${r.id})" title="Просмотр">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить">
|
<button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`}).join('');
|
`}).join('');
|
||||||
@@ -1061,77 +973,83 @@
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Ошибка загрузки резюме:', error);
|
console.error('❌ Ошибка загрузки резюме:', error);
|
||||||
document.getElementById('resumesTable').innerHTML = '<tr><td colspan="9" style="text-align: center;">Ошибка загрузки резюме</td></tr>';
|
document.getElementById('resumesTable').innerHTML = '<tr><td colspan="9" style="text-align: center;">Ошибка загрузки</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Резервный метод загрузки резюме
|
// ========== ЗАГРУЗКА КОМПАНИЙ ==========
|
||||||
async function loadResumesFallback(page = 1, search = '') {
|
async function loadCompanies(page = 1, search = '') {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Используем резервный метод загрузки резюме');
|
console.log(`📥 Загрузка компаний: страница ${page}, поиск "${search}"`);
|
||||||
|
|
||||||
let url = `${API_BASE_URL}/resumes/all?page=${page}&limit=10`;
|
let url = `${API_BASE_URL}/admin/companies?page=${page}&limit=10`;
|
||||||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Ошибка загрузки резюме');
|
if (!response.ok) throw new Error('Ошибка загрузки компаний');
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('✅ Резюме загружены (резервный метод):', data);
|
const companies = data.companies || data;
|
||||||
|
const totalPages = data.total_pages || Math.ceil((data.total || companies.length) / 10) || 1;
|
||||||
|
|
||||||
const resumes = data.resumes || [];
|
const table = document.getElementById('companiesTable');
|
||||||
const totalPages = data.total_pages || 1;
|
|
||||||
|
|
||||||
const table = document.getElementById('resumesTable');
|
if (!companies || companies.length === 0) {
|
||||||
|
table.innerHTML = '<tr><td colspan="10" style="text-align: center;">Компании не найдены</td></tr>';
|
||||||
if (!resumes || resumes.length === 0) {
|
|
||||||
table.innerHTML = '<tr><td colspan="9" style="text-align: center;">Резюме не найдены</td></tr>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.innerHTML = resumes.map(r => {
|
table.innerHTML = companies.map(c => `
|
||||||
const expCount = r.experience_count || 0;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>${r.id}</td>
|
<td>${c.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>
|
<td>
|
||||||
${(r.tags || []).map(t =>
|
<div class="company-logo">
|
||||||
`<span class="badge" style="background: #eef4fa; color: #1f3f60; margin: 2px;">${escapeHtml(t.name || t)}</span>`
|
${c.logo ?
|
||||||
).join('')}
|
`<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>
|
||||||
<td>${r.views || 0}</td>
|
|
||||||
<td>${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<button class="btn-icon view" onclick="viewResume(${r.id})" title="Просмотр">
|
<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>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить">
|
<button class="btn-icon delete" onclick="deleteCompany(${c.id})" title="Удалить">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`}).join('');
|
`).join('');
|
||||||
|
|
||||||
renderPagination('resumes', totalPages, page);
|
renderPagination('companies', totalPages, page);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Ошибка резервной загрузки резюме:', error);
|
console.error('❌ Ошибка загрузки компаний:', error);
|
||||||
throw error;
|
document.getElementById('companiesTable').innerHTML = '<tr><td colspan="10" style="text-align: center;">Ошибка загрузки</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательная функция для склонения
|
|
||||||
function getDeclension(number, titles) {
|
|
||||||
const cases = [2, 0, 1, 1, 1, 2];
|
|
||||||
return titles[ (number % 100 > 4 && number % 100 < 20) ? 2 : cases[(number % 10 < 5) ? number % 10 : 5] ];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== ПРОСМОТР РЕЗЮМЕ ==========
|
// ========== ПРОСМОТР РЕЗЮМЕ ==========
|
||||||
async function viewResume(resumeId) {
|
async function viewResume(resumeId) {
|
||||||
try {
|
try {
|
||||||
@@ -1235,29 +1153,104 @@
|
|||||||
document.getElementById('resumeViewModal').classList.remove('active');
|
document.getElementById('resumeViewModal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== УДАЛЕНИЕ РЕЗЮМЕ ==========
|
// ========== ПРОСМОТР КОМПАНИИ ==========
|
||||||
async function deleteResume(resumeId) {
|
async function viewCompany(companyId) {
|
||||||
if (!confirm('Вы уверены, что хотите удалить это резюме? Это действие нельзя отменить.')) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/admin/resumes/${resumeId}`, {
|
const response = await fetch(`${API_BASE_URL}/companies/${companyId}`);
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Ошибка удаления');
|
if (!response.ok) throw new Error('Ошибка загрузки компании');
|
||||||
|
|
||||||
showNotification('Резюме удалено', 'success');
|
const company = await response.json();
|
||||||
loadResumes(currentPage.resumes, searchTerms.resumes);
|
|
||||||
loadStats(); // Обновляем статистику
|
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) {
|
} catch (error) {
|
||||||
console.error('Error deleting resume:', error);
|
console.error('Error loading company details:', error);
|
||||||
showNotification('Ошибка при удалении резюме', 'error');
|
showNotification('Ошибка загрузки деталей компании', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== УДАЛЕНИЕ ПОЛЬЗОВАТЕЛЯ ==========
|
function closeCompanyModal() {
|
||||||
|
document.getElementById('companyViewModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== УДАЛЕНИЕ ==========
|
||||||
async function deleteUser(userId) {
|
async function deleteUser(userId) {
|
||||||
if (!confirm('Удалить пользователя? Это действие нельзя отменить.')) return;
|
if (!confirm('Удалить пользователя? Это действие нельзя отменить.')) return;
|
||||||
|
|
||||||
@@ -1279,7 +1272,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== УДАЛЕНИЕ ВАКАНСИИ ==========
|
|
||||||
async function deleteVacancy(vacancyId) {
|
async function deleteVacancy(vacancyId) {
|
||||||
if (!confirm('Удалить вакансию?')) return;
|
if (!confirm('Удалить вакансию?')) return;
|
||||||
|
|
||||||
@@ -1301,13 +1293,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ПРОСМОТР ВАКАНСИИ ==========
|
async function deleteResume(resumeId) {
|
||||||
function viewVacancy(vacancyId) {
|
if (!confirm('Вы уверены, что хотите удалить это резюме? Это действие нельзя отменить.')) return;
|
||||||
window.open(`/vacancy/${vacancyId}`, '_blank');
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ПАГИНАЦИЯ ==========
|
// ========== ПАГИНАЦИЯ ==========
|
||||||
function renderPagination(type, totalPages, currentPage) {
|
function renderPagination(type, totalPages, currentPageNum) {
|
||||||
const container = document.getElementById(`${type}Pagination`);
|
const container = document.getElementById(`${type}Pagination`);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -1318,7 +1347,7 @@
|
|||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="goToPage('${type}', ${i})">${i}</button>`;
|
html += `<button class="page-btn ${i === currentPageNum ? 'active' : ''}" onclick="goToPage('${type}', ${i})">${i}</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
@@ -1337,6 +1366,9 @@
|
|||||||
case 'resumes':
|
case 'resumes':
|
||||||
loadResumes(page, searchTerms.resumes);
|
loadResumes(page, searchTerms.resumes);
|
||||||
break;
|
break;
|
||||||
|
case 'companies':
|
||||||
|
loadCompanies(page, searchTerms.companies);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1360,6 +1392,9 @@
|
|||||||
case 'resumes':
|
case 'resumes':
|
||||||
loadResumes(1, searchTerms[type]);
|
loadResumes(1, searchTerms[type]);
|
||||||
break;
|
break;
|
||||||
|
case 'companies':
|
||||||
|
loadCompanies(1, searchTerms[type]);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -1370,8 +1405,11 @@
|
|||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
document.querySelectorAll('.content-card > div').forEach(d => d.style.display = 'none');
|
document.querySelectorAll('.content-card > div').forEach(d => d.style.display = 'none');
|
||||||
|
|
||||||
const activeTab = document.querySelector(`.tab:nth-child(${tab === 'users' ? 1 : tab === 'vacancies' ? 2 : tab === 'resumes' ? 3 : 4})`);
|
const tabMap = { 'users': 0, 'vacancies': 1, 'resumes': 2, 'companies': 3, 'tags': 4 };
|
||||||
if (activeTab) activeTab.classList.add('active');
|
const index = tabMap[tab];
|
||||||
|
if (index !== undefined) {
|
||||||
|
document.querySelectorAll('.tab')[index]?.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
if (tab === 'users') {
|
if (tab === 'users') {
|
||||||
document.getElementById('usersTab').style.display = 'block';
|
document.getElementById('usersTab').style.display = 'block';
|
||||||
@@ -1382,19 +1420,18 @@
|
|||||||
} else if (tab === 'resumes') {
|
} else if (tab === 'resumes') {
|
||||||
document.getElementById('resumesTab').style.display = 'block';
|
document.getElementById('resumesTab').style.display = 'block';
|
||||||
loadResumes(currentPage.resumes, searchTerms.resumes);
|
loadResumes(currentPage.resumes, searchTerms.resumes);
|
||||||
|
} else if (tab === 'companies') {
|
||||||
|
document.getElementById('companiesTab').style.display = 'block';
|
||||||
|
loadCompanies(currentPage.companies, searchTerms.companies);
|
||||||
} else if (tab === 'tags') {
|
} else if (tab === 'tags') {
|
||||||
document.getElementById('tagsTab').style.display = 'block';
|
document.getElementById('tagsTab').style.display = 'block';
|
||||||
|
loadPopularTags();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ФИЛЬТРАЦИЯ ПО СТАТИСТИКЕ ==========
|
// ========== ФИЛЬТРАЦИЯ ПО СТАТИСТИКЕ ==========
|
||||||
function filterByType(type) {
|
function filterByType(type) {
|
||||||
if (type === 'users') {
|
if (type === 'users' || type === 'employees' || type === 'employers') {
|
||||||
switchTab('users');
|
|
||||||
} else if (type === 'employees') {
|
|
||||||
switchTab('users');
|
|
||||||
// Здесь можно добавить фильтр по роли
|
|
||||||
} else if (type === 'employers') {
|
|
||||||
switchTab('users');
|
switchTab('users');
|
||||||
} else if (type === 'vacancies') {
|
} else if (type === 'vacancies') {
|
||||||
switchTab('vacancies');
|
switchTab('vacancies');
|
||||||
@@ -1406,6 +1443,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== РАБОТА С ТЕГАМИ ==========
|
// ========== РАБОТА С ТЕГАМИ ==========
|
||||||
|
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>
|
||||||
|
<td>0</td>
|
||||||
|
<td>0</td>
|
||||||
|
<td>0</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading tags:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showAddTagModal() {
|
function showAddTagModal() {
|
||||||
document.getElementById('tagModal').classList.add('active');
|
document.getElementById('tagModal').classList.add('active');
|
||||||
}
|
}
|
||||||
@@ -1441,7 +1507,7 @@
|
|||||||
|
|
||||||
showNotification('Тег создан', 'success');
|
showNotification('Тег создан', 'success');
|
||||||
closeTagModal();
|
closeTagModal();
|
||||||
loadStats(); // Обновляем статистику
|
loadPopularTags();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating tag:', error);
|
console.error('Error creating tag:', error);
|
||||||
@@ -1449,7 +1515,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ЭКРАНИРОВАНИЕ HTML ==========
|
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
|
||||||
|
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) {
|
function escapeHtml(unsafe) {
|
||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
return unsafe.toString()
|
return unsafe.toString()
|
||||||
@@ -1483,6 +1554,7 @@
|
|||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const tagModal = document.getElementById('tagModal');
|
const tagModal = document.getElementById('tagModal');
|
||||||
const resumeModal = document.getElementById('resumeViewModal');
|
const resumeModal = document.getElementById('resumeViewModal');
|
||||||
|
const companyModal = document.getElementById('companyViewModal');
|
||||||
|
|
||||||
if (event.target === tagModal) {
|
if (event.target === tagModal) {
|
||||||
tagModal.classList.remove('active');
|
tagModal.classList.remove('active');
|
||||||
@@ -1490,6 +1562,9 @@
|
|||||||
if (event.target === resumeModal) {
|
if (event.target === resumeModal) {
|
||||||
resumeModal.classList.remove('active');
|
resumeModal.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
if (event.target === companyModal) {
|
||||||
|
companyModal.classList.remove('active');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== ИНИЦИАЛИЗАЦИЯ ==========
|
// ========== ИНИЦИАЛИЗАЦИЯ ==========
|
||||||
@@ -1497,7 +1572,7 @@
|
|||||||
updateNavigation();
|
updateNavigation();
|
||||||
loadStats();
|
loadStats();
|
||||||
loadUsers();
|
loadUsers();
|
||||||
// Не загружаем все сразу, только активный таб
|
loadCompanies();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user