admin company page

This commit is contained in:
2026-03-25 19:06:40 +03:00
parent 28d372af80
commit b5c4e3210e
2 changed files with 439 additions and 245 deletions

119
server.py
View File

@@ -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 (публичный эндпоинт)"""

View File

@@ -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()">&times;</button> <button class="modal-close" onclick="closeCompanyModal()">&times;</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;">
<i class="fas fa-eye"></i> <button class="btn-icon view" onclick="window.open('/vacancy/${v.id}', '_blank')" title="Просмотр">
</button> <i class="fas fa-eye"></i>
<button class="btn-icon delete" onclick="deleteVacancy(${v.id})" title="Удалить"> </button>
<i class="fas fa-trash"></i> <button class="btn-icon delete" onclick="deleteVacancy(${v.id})" title="Удалить">
</button> <i class="fas fa-trash"></i>
</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>
<button class="btn-icon view" onclick="viewResume(${r.id})" title="Просмотр"> <div style="display: flex; gap: 5px;">
<i class="fas fa-eye"></i> <button class="btn-icon view" onclick="viewResume(${r.id})" title="Просмотр">
</button> <i class="fas fa-eye"></i>
<button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить"> </button>
<i class="fas fa-trash"></i> <button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить">
</button> <i class="fas fa-trash"></i>
</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;">
<i class="fas fa-eye"></i> ${escapeHtml(c.name)}
</button> <i class="fas fa-external-link-alt" style="font-size: 12px; color: #4f7092;"></i>
<button class="btn-icon delete" onclick="deleteResume(${r.id})" title="Удалить"> </a>
<i class="fas fa-trash"></i> </td>
</button> <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> </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>