diff --git a/server.py b/server.py
index c4c3f90..e20a35f 100644
--- a/server.py
+++ b/server.py
@@ -2161,6 +2161,125 @@ async def create_or_update_company(
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}")
async def get_company_by_id(company_id: int):
"""Получение информации о компании по ID (публичный эндпоинт)"""
diff --git a/templates/admin.html b/templates/admin.html
index ad81701..59b1f81 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -284,29 +284,23 @@
background: #bae6fd;
}
- .tags-cloud {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- margin: 20px 0;
- }
-
- .tag-item {
+ .company-logo {
+ width: 40px;
+ height: 40px;
background: #eef4fa;
- padding: 8px 16px;
- border-radius: 30px;
+ border-radius: 10px;
display: flex;
align-items: center;
- gap: 8px;
- font-size: 14px;
+ justify-content: center;
+ font-size: 20px;
+ color: #3b82f6;
+ overflow: hidden;
}
- .tag-item .count {
- background: #3b82f6;
- color: white;
- padding: 2px 8px;
- border-radius: 20px;
- font-size: 12px;
+ .company-logo img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
}
.loading {
@@ -402,26 +396,6 @@
color: #0b1c34;
}
- .experience-list, .education-list {
- margin-top: 10px;
- }
-
- .exp-item, .edu-item {
- background: #f9fcff;
- padding: 15px;
- border-radius: 15px;
- margin-bottom: 10px;
- }
-
- .exp-item strong, .edu-item strong {
- color: #0b1c34;
- }
-
- .exp-item .period, .edu-item .year {
- color: #4f7092;
- font-size: 12px;
- }
-
.notification {
position: fixed;
top: 20px;
@@ -488,90 +462,6 @@
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;
- }
@@ -619,6 +509,7 @@
+
+
-
-
-
-
+
+
Информация о компании
+
@@ -793,13 +713,15 @@
let currentPage = {
users: 1,
vacancies: 1,
- resumes: 1
+ resumes: 1,
+ companies: 1
};
let searchTerms = {
users: '',
vacancies: '',
- resumes: ''
+ resumes: '',
+ companies: ''
};
let searchTimeouts = {};
@@ -807,6 +729,7 @@
// ========== УВЕДОМЛЕНИЯ ==========
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
+ if (!notification) return;
notification.className = `notification ${type}`;
notification.innerHTML = message;
notification.style.display = 'block';
@@ -860,26 +783,17 @@
async function loadUsers(page = 1, search = '') {
try {
console.log(`📥 Загрузка пользователей: страница ${page}, поиск "${search}"`);
- console.log('🔑 Токен:', token ? 'Есть' : 'Нет');
let url = `${API_BASE_URL}/admin/users?page=${page}&limit=10`;
if (search) url += `&search=${encodeURIComponent(search)}`;
const response = await fetch(url, {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- }
+ headers: { 'Authorization': `Bearer ${token}` }
});
- if (!response.ok) {
- console.error('❌ Ошибка ответа:', response.status, response.statusText);
- throw new Error('Ошибка загрузки пользователей');
- }
+ if (!response.ok) throw new Error('Ошибка загрузки пользователей');
const data = await response.json();
- console.log('✅ Данные пользователей:', data);
-
const users = data.users || data;
const totalPages = data.total_pages || Math.ceil((data.total || users.length) / 10) || 1;
@@ -940,7 +854,9 @@
// ========== ЗАГРУЗКА ВАКАНСИЙ ==========
async function loadVacancies(page = 1, search = '') {
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)}`;
const response = await fetch(url, {
@@ -951,6 +867,7 @@
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');
@@ -978,20 +895,22 @@
${v.views || 0} |
${new Date(v.created_at).toLocaleDateString()} |
-
-
+
+
+
+
|
`).join('');
- renderPagination('vacancies', data.total_pages || 1, page);
+ renderPagination('vacancies', totalPages, page);
} catch (error) {
- console.error('Error loading vacancies:', error);
+ console.error('❌ Ошибка загрузки вакансий:', error);
document.getElementById('vacanciesTable').innerHTML = '
| Ошибка загрузки |
';
}
}
@@ -1008,16 +927,9 @@
headers: { 'Authorization': `Bearer ${token}` }
});
- if (!response.ok) {
- console.warn('Эндпоинт admin/resumes не найден, пробуем резервный вариант');
- // Пробуем загрузить через публичный эндпоинт
- await loadResumesFallback(page, search);
- return;
- }
+ if (!response.ok) throw new Error('Ошибка загрузки резюме');
const data = await response.json();
- console.log('✅ Резюме загружены:', data);
-
const resumes = data.resumes || data;
const totalPages = data.total_pages || Math.ceil((data.total || resumes.length) / 10) || 1;
@@ -1029,9 +941,7 @@
}
table.innerHTML = resumes.map(r => {
- // Безопасное получение опыта
const expCount = r.work_experience ? r.work_experience.length : (r.experience_count || 0);
-
return `
| ${r.id} |
@@ -1047,12 +957,14 @@
${r.views || 0} |
${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'} |
-
-
+
+
+
+
|
`}).join('');
@@ -1061,77 +973,83 @@
} catch (error) {
console.error('❌ Ошибка загрузки резюме:', error);
- document.getElementById('resumesTable').innerHTML = '
| Ошибка загрузки резюме |
';
+ document.getElementById('resumesTable').innerHTML = '
| Ошибка загрузки |
';
}
}
- // Резервный метод загрузки резюме
- async function loadResumesFallback(page = 1, search = '') {
+ // ========== ЗАГРУЗКА КОМПАНИЙ ==========
+ async function loadCompanies(page = 1, search = '') {
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)}`;
- 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();
- 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 totalPages = data.total_pages || 1;
+ const table = document.getElementById('companiesTable');
- const table = document.getElementById('resumesTable');
-
- if (!resumes || resumes.length === 0) {
- table.innerHTML = '
| Резюме не найдены |
';
+ if (!companies || companies.length === 0) {
+ table.innerHTML = '
| Компании не найдены |
';
return;
}
- table.innerHTML = resumes.map(r => {
- const expCount = r.experience_count || 0;
-
- return `
+ table.innerHTML = companies.map(c => `
- | ${r.id} |
- ${escapeHtml(r.full_name || '—')} |
- ${escapeHtml(r.desired_position || '—')} |
- ${escapeHtml(r.desired_salary || '—')} |
- ${expCount} ${getDeclension(expCount, ['место', 'места', 'мест'])} |
+ ${c.id} |
- ${(r.tags || []).map(t =>
- `${escapeHtml(t.name || t)}`
- ).join('')}
+
+ ${c.logo ?
+ ` }) ` :
+ ` `
+ }
+
|
- ${r.views || 0} |
- ${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'} |
-
-
+
+ ${escapeHtml(c.name)}
+
+
+ |
+
+
+ ${escapeHtml(c.owner_name || '—')}
+
+ |
+ ${escapeHtml(c.email || '—')} |
+ ${escapeHtml(c.phone || '—')} |
+ ${c.website ? `${escapeHtml(c.website)}` : '—'} |
+ ${c.vacancies_count || 0} |
+ ${new Date(c.created_at).toLocaleDateString()} |
+
+
+
+
+
|
- `}).join('');
+ `).join('');
- renderPagination('resumes', totalPages, page);
+ renderPagination('companies', totalPages, page);
} catch (error) {
- console.error('❌ Ошибка резервной загрузки резюме:', error);
- throw error;
+ console.error('❌ Ошибка загрузки компаний:', error);
+ document.getElementById('companiesTable').innerHTML = '
| Ошибка загрузки |
';
}
}
- // Вспомогательная функция для склонения
- function getDeclension(number, titles) {
- const cases = [2, 0, 1, 1, 1, 2];
- return titles[ (number % 100 > 4 && number % 100 < 20) ? 2 : cases[(number % 10 < 5) ? number % 10 : 5] ];
- }
-
// ========== ПРОСМОТР РЕЗЮМЕ ==========
async function viewResume(resumeId) {
try {
@@ -1235,29 +1153,104 @@
document.getElementById('resumeViewModal').classList.remove('active');
}
- // ========== УДАЛЕНИЕ РЕЗЮМЕ ==========
- async function deleteResume(resumeId) {
- if (!confirm('Вы уверены, что хотите удалить это резюме? Это действие нельзя отменить.')) return;
-
+ // ========== ПРОСМОТР КОМПАНИИ ==========
+ async function viewCompany(companyId) {
try {
- const response = await fetch(`${API_BASE_URL}/admin/resumes/${resumeId}`, {
- method: 'DELETE',
- headers: { 'Authorization': `Bearer ${token}` }
- });
+ const response = await fetch(`${API_BASE_URL}/companies/${companyId}`);
- if (!response.ok) throw new Error('Ошибка удаления');
+ if (!response.ok) throw new Error('Ошибка загрузки компании');
- showNotification('Резюме удалено', 'success');
- loadResumes(currentPage.resumes, searchTerms.resumes);
- loadStats(); // Обновляем статистику
+ 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 = `
+
+
Основная информация
+
+
+
Название
+
${escapeHtml(company.name)}
+
+
+
+
Дата регистрации
+
${new Date(company.created_at).toLocaleDateString()}
+
+
+
Активных вакансий
+
${vacanciesData.vacancies?.filter(v => v.is_active).length || 0}
+
+
+
+
+
+
Контакты
+
+
+
Email
+
${escapeHtml(company.email || '—')}
+
+
+
Телефон
+
${escapeHtml(company.phone || '—')}
+
+
+
Адрес
+
${escapeHtml(company.address || '—')}
+
+
+
+
+
+ ${company.description ? `
+
+
Описание компании
+
+ ${escapeHtml(company.description).replace(/\n/g, '
')}
+
+
+ ` : ''}
+
+
+
Последние вакансии (${vacanciesData.vacancies?.length || 0})
+ ${vacanciesData.vacancies && vacanciesData.vacancies.length > 0 ?
+ vacanciesData.vacancies.slice(0, 5).map(v => `
+
+
${escapeHtml(v.title)}
+
${escapeHtml(v.salary || 'з/п не указана')}
+
${v.is_active ? 'Активна' : 'Неактивна'}
+
+ `).join('') :
+ '
Нет вакансий
'
+ }
+
+ `;
+
+ document.getElementById('companyViewModal').classList.add('active');
} catch (error) {
- console.error('Error deleting resume:', error);
- showNotification('Ошибка при удалении резюме', '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;
@@ -1279,7 +1272,6 @@
}
}
- // ========== УДАЛЕНИЕ ВАКАНСИИ ==========
async function deleteVacancy(vacancyId) {
if (!confirm('Удалить вакансию?')) return;
@@ -1301,13 +1293,50 @@
}
}
- // ========== ПРОСМОТР ВАКАНСИИ ==========
- function viewVacancy(vacancyId) {
- window.open(`/vacancy/${vacancyId}`, '_blank');
+ 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');
+ }
}
// ========== ПАГИНАЦИЯ ==========
- function renderPagination(type, totalPages, currentPage) {
+ function renderPagination(type, totalPages, currentPageNum) {
const container = document.getElementById(`${type}Pagination`);
if (!container) return;
@@ -1318,7 +1347,7 @@
let html = '';
for (let i = 1; i <= totalPages; i++) {
- html += `
`;
+ html += `
`;
}
container.innerHTML = html;
@@ -1337,6 +1366,9 @@
case 'resumes':
loadResumes(page, searchTerms.resumes);
break;
+ case 'companies':
+ loadCompanies(page, searchTerms.companies);
+ break;
}
}
@@ -1360,6 +1392,9 @@
case 'resumes':
loadResumes(1, searchTerms[type]);
break;
+ case 'companies':
+ loadCompanies(1, searchTerms[type]);
+ break;
}
}
}, 500);
@@ -1370,8 +1405,11 @@
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.content-card > div').forEach(d => d.style.display = 'none');
- const activeTab = document.querySelector(`.tab:nth-child(${tab === 'users' ? 1 : tab === 'vacancies' ? 2 : tab === 'resumes' ? 3 : 4})`);
- if (activeTab) activeTab.classList.add('active');
+ 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';
@@ -1382,19 +1420,18 @@
} 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') {
- switchTab('users');
- } else if (type === 'employees') {
- switchTab('users');
- // Здесь можно добавить фильтр по роли
- } else if (type === 'employers') {
+ if (type === 'users' || type === 'employees' || type === 'employers') {
switchTab('users');
} else if (type === '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 => `
+
+ ${escapeHtml(t.name)}
+ 0
+
+ `).join('');
+
+ const popularTagsTable = document.getElementById('popularTagsTable');
+ popularTagsTable.innerHTML = tags.slice(0, 10).map(t => `
+
+ | ${escapeHtml(t.name)} |
+ ${escapeHtml(t.category || '—')} |
+ 0 |
+ 0 |
+ 0 |
+
+ `).join('');
+
+ } catch (error) {
+ console.error('Error loading tags:', error);
+ }
+ }
+
function showAddTagModal() {
document.getElementById('tagModal').classList.add('active');
}
@@ -1441,7 +1507,7 @@
showNotification('Тег создан', 'success');
closeTagModal();
- loadStats(); // Обновляем статистику
+ loadPopularTags();
} catch (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) {
if (!unsafe) return '';
return unsafe.toString()
@@ -1483,6 +1554,7 @@
window.onclick = function(event) {
const tagModal = document.getElementById('tagModal');
const resumeModal = document.getElementById('resumeViewModal');
+ const companyModal = document.getElementById('companyViewModal');
if (event.target === tagModal) {
tagModal.classList.remove('active');
@@ -1490,6 +1562,9 @@
if (event.target === resumeModal) {
resumeModal.classList.remove('active');
}
+ if (event.target === companyModal) {
+ companyModal.classList.remove('active');
+ }
};
// ========== ИНИЦИАЛИЗАЦИЯ ==========
@@ -1497,7 +1572,7 @@
updateNavigation();
loadStats();
loadUsers();
- // Не загружаем все сразу, только активный таб
+ loadCompanies();
});