admin resume and vacancy edit

This commit is contained in:
2026-03-26 19:00:41 +03:00
parent b5c4e3210e
commit 5747a063b2
3 changed files with 1364 additions and 375 deletions

434
server.py
View File

@@ -4,7 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr, ConfigDict
from typing import Optional, List from typing import Optional, List
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from jose import jwt from jose import jwt
@@ -318,9 +318,10 @@ class TokenResponse(BaseModel):
class Tag(BaseModel): class Tag(BaseModel):
id: Optional[int] = None model_config = ConfigDict(from_attributes=True)
id: int
name: str name: str
category: str = "skill" category: Optional[str] = None
class VacancyCreate(BaseModel): class VacancyCreate(BaseModel):
@@ -383,7 +384,7 @@ class ResumeResponse(BaseModel):
desired_salary: Optional[str] desired_salary: Optional[str]
work_experience: List[WorkExperience] work_experience: List[WorkExperience]
education: List[Education] education: List[Education]
tags: List[Tag] = [] tags: List[Tag]
views: int views: int
updated_at: str updated_at: str
full_name: Optional[str] = None full_name: Optional[str] = None
@@ -1426,25 +1427,32 @@ async def update_vacancy(
vacancy_update: VacancyUpdate, vacancy_update: VacancyUpdate,
user_id: int = Depends(get_current_user) user_id: int = Depends(get_current_user)
): ):
"""Обновление существующей вакансии""" """Обновление существующей вакансии (для админа и владельца)"""
try:
print(f"📝 Обновление вакансии {vacancy_id} пользователем {user_id}")
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Проверяем, что вакансия принадлежит пользователю # Проверяем существование вакансии
cursor.execute(""" cursor.execute("SELECT * FROM vacancies WHERE id = ?", (vacancy_id,))
SELECT v.*, u.role
FROM vacancies v
JOIN users u ON v.user_id = u.id
WHERE v.id = ? AND v.user_id = ?
""", (vacancy_id, user_id))
vacancy = cursor.fetchone() vacancy = cursor.fetchone()
if not vacancy: if not vacancy:
raise HTTPException(status_code=404, detail="Вакансия не найдена") raise HTTPException(status_code=404, detail="Вакансия не найдена")
# Проверяем роль (только работодатель может редактировать) # Проверяем права (админ или владелец)
if vacancy["role"] != "employer": cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
raise HTTPException(status_code=403, detail="Только работодатели могут редактировать вакансии") current_user = cursor.fetchone()
is_admin = current_user and current_user["is_admin"] == 1
# Если не админ и не владелец - запрещаем
if not is_admin and vacancy["user_id"] != user_id:
print(
f"❌ Доступ запрещен: пользователь {user_id} не является админом или владельцем вакансии {vacancy_id}")
raise HTTPException(status_code=403, detail="Нет прав на редактирование")
print(f"✅ Права проверены: is_admin={is_admin}, is_owner={vacancy['user_id'] == user_id}")
# Обновляем основные поля # Обновляем основные поля
update_fields = [] update_fields = []
@@ -1474,6 +1482,7 @@ async def update_vacancy(
query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?" query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?"
params.append(vacancy_id) params.append(vacancy_id)
cursor.execute(query, params) cursor.execute(query, params)
print(f"✅ Основные поля вакансии {vacancy_id} обновлены")
# Обновляем теги # Обновляем теги
if vacancy_update.tags is not None: if vacancy_update.tags is not None:
@@ -1482,6 +1491,8 @@ async def update_vacancy(
# Добавляем новые теги # Добавляем новые теги
for tag_name in vacancy_update.tags: for tag_name in vacancy_update.tags:
if tag_name and tag_name.strip():
tag_name = tag_name.strip()
# Ищем или создаем тег # Ищем или создаем тег
cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)) cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,))
tag = cursor.fetchone() tag = cursor.fetchone()
@@ -1497,11 +1508,15 @@ async def update_vacancy(
VALUES (?, ?) VALUES (?, ?)
""", (vacancy_id, tag_id)) """, (vacancy_id, tag_id))
print(f"✅ Теги вакансии {vacancy_id} обновлены")
conn.commit() conn.commit()
# Возвращаем обновленную вакансию # Возвращаем обновленную вакансию
cursor.execute(""" cursor.execute("""
SELECT v.*, COALESCE(c.name, u.full_name) as company_name SELECT v.*,
COALESCE(c.name, u.full_name) as company_name,
u.full_name as user_name
FROM vacancies v FROM vacancies v
JOIN users u ON v.user_id = u.id JOIN users u ON v.user_id = u.id
LEFT JOIN companies c ON v.user_id = c.user_id LEFT JOIN companies c ON v.user_id = c.user_id
@@ -1512,14 +1527,22 @@ async def update_vacancy(
# Добавляем теги # Добавляем теги
cursor.execute(""" cursor.execute("""
SELECT t.* FROM tags t SELECT t.name FROM tags t
JOIN vacancy_tags vt ON t.id = vt.tag_id JOIN vacancy_tags vt ON t.id = vt.tag_id
WHERE vt.vacancy_id = ? WHERE vt.vacancy_id = ?
""", (vacancy_id,)) """, (vacancy_id,))
updated_vacancy["tags"] = [dict(tag) for tag in cursor.fetchall()] updated_vacancy["tags"] = [tag["name"] for tag in cursor.fetchall()]
print(f"✅ Вакансия {vacancy_id} успешно обновлена")
return updated_vacancy return updated_vacancy
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при обновлении вакансии {vacancy_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.post("/api/vacancies/{vacancy_id}/apply") @app.post("/api/vacancies/{vacancy_id}/apply")
async def apply_to_vacancy( async def apply_to_vacancy(
@@ -1558,17 +1581,28 @@ async def create_or_update_resume(
resume: ResumeCreate, resume: ResumeCreate,
user_id: int = Depends(get_current_user) user_id: int = Depends(get_current_user)
): ):
"""Создание или обновление резюме""" """Создание или обновление резюме (для админа и владельца)"""
try:
print(f"📝 Сохранение резюме для пользователя {user_id}")
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Проверка роли # Получаем информацию о текущем пользователе
cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) cursor.execute("SELECT role, is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone() current_user = cursor.fetchone()
if not user or user["role"] != "employee":
raise HTTPException(status_code=403, detail="Только соискатели могут создавать резюме")
# Проверка существования резюме if not current_user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
is_admin = current_user["is_admin"] == 1
is_employee = current_user["role"] == "employee"
# Если пользователь не админ и не соискатель
if not is_admin and not is_employee:
raise HTTPException(status_code=403, detail="Только соискатели и администраторы могут создавать резюме")
# Проверяем существование резюме
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,)) cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,))
existing = cursor.fetchone() existing = cursor.fetchone()
@@ -1586,6 +1620,8 @@ async def create_or_update_resume(
cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
print(f"✅ Обновлено существующее резюме ID {resume_id} для пользователя {user_id}")
else: else:
# Создание нового резюме # Создание нового резюме
cursor.execute(""" cursor.execute("""
@@ -1594,8 +1630,9 @@ async def create_or_update_resume(
""", (user_id, resume.desired_position, resume.about_me, resume.desired_salary)) """, (user_id, resume.desired_position, resume.about_me, resume.desired_salary))
resume_id = cursor.lastrowid resume_id = cursor.lastrowid
print(f"✅ Создано новое резюме ID {resume_id} для пользователя {user_id}")
# Добавление опыта работы (ОБНОВЛЕНО - добавили description) # Добавление опыта работы
for exp in resume.work_experience: for exp in resume.work_experience:
cursor.execute(""" cursor.execute("""
INSERT INTO work_experience (resume_id, position, company, period, description) INSERT INTO work_experience (resume_id, position, company, period, description)
@@ -1612,6 +1649,9 @@ async def create_or_update_resume(
# Добавление тегов # Добавление тегов
if resume.tags: if resume.tags:
for tag_name in resume.tags: for tag_name in resume.tags:
if tag_name and tag_name.strip():
tag_name = tag_name.strip()
# Ищем или создаем тег
cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)) cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,))
tag = cursor.fetchone() tag = cursor.fetchone()
@@ -1628,14 +1668,20 @@ async def create_or_update_resume(
conn.commit() conn.commit()
# Получение обновленного резюме # Получение обновленного резюме - ВАЖНО: возвращаем теги в правильном формате
cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,)) cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,))
resume_data = dict(cursor.fetchone()) resume_data = dict(cursor.fetchone())
# Получаем имя пользователя
cursor.execute("SELECT full_name FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
resume_data["full_name"] = user["full_name"] if user else None
cursor.execute(""" cursor.execute("""
SELECT position, company, period, description SELECT position, company, period, description
FROM work_experience FROM work_experience
WHERE resume_id = ? WHERE resume_id = ?
ORDER BY period DESC
""", (resume_id,)) """, (resume_id,))
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
@@ -1643,18 +1689,29 @@ async def create_or_update_resume(
SELECT institution, specialty, graduation_year SELECT institution, specialty, graduation_year
FROM education FROM education
WHERE resume_id = ? WHERE resume_id = ?
ORDER BY graduation_year DESC
""", (resume_id,)) """, (resume_id,))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
# ВАЖНО: теги возвращаем в формате списка объектов с полями id, name, category
cursor.execute(""" cursor.execute("""
SELECT t.* FROM tags t SELECT t.id, t.name, t.category FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ? WHERE rt.resume_id = ?
""", (resume_id,)) """, (resume_id,))
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()] resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()]
print(f"✅ Резюме {resume_id} успешно сохранено")
print(f"📌 Теги в ответе: {resume_data['tags']}")
return resume_data return resume_data
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при сохранении резюме: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.get("/api/resume", response_model=ResumeResponse) @app.get("/api/resume", response_model=ResumeResponse)
async def get_my_resume(user_id: int = Depends(get_current_user)): async def get_my_resume(user_id: int = Depends(get_current_user)):
@@ -1662,6 +1719,14 @@ async def get_my_resume(user_id: int = Depends(get_current_user)):
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Получаем информацию о пользователе
cursor.execute("SELECT role, is_admin FROM users WHERE id = ?", (user_id,))
current_user = cursor.fetchone()
if not current_user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Если админ, он может видеть свое резюме, если оно есть
cursor.execute("SELECT * FROM resumes WHERE user_id = ?", (user_id,)) cursor.execute("SELECT * FROM resumes WHERE user_id = ?", (user_id,))
resume = cursor.fetchone() resume = cursor.fetchone()
@@ -1670,21 +1735,14 @@ async def get_my_resume(user_id: int = Depends(get_current_user)):
resume_data = dict(resume) resume_data = dict(resume)
# Получаем опыт работы (ОБНОВЛЕНО - добавили поле description)
cursor.execute(""" cursor.execute("""
SELECT position, company, period, description SELECT position, company, period, description
FROM work_experience FROM work_experience
WHERE resume_id = ? WHERE resume_id = ?
ORDER BY ORDER BY period DESC
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume["id"],)) """, (resume["id"],))
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
# Получаем образование
cursor.execute(""" cursor.execute("""
SELECT institution, specialty, graduation_year SELECT institution, specialty, graduation_year
FROM education FROM education
@@ -1693,13 +1751,12 @@ async def get_my_resume(user_id: int = Depends(get_current_user)):
""", (resume["id"],)) """, (resume["id"],))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
# Получаем теги
cursor.execute(""" cursor.execute("""
SELECT t.* FROM tags t SELECT t.name FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ? WHERE rt.resume_id = ?
""", (resume["id"],)) """, (resume["id"],))
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()] resume_data["tags"] = [tag["name"] for tag in cursor.fetchall()]
return resume_data return resume_data
@@ -1780,9 +1837,9 @@ async def get_all_resumes_admin(
limit: int = 10, limit: int = 10,
search: str = None search: str = None
): ):
"""Получение всех резюме для админки с пагинацией и поиском""" """Получение всех резюме для админки"""
try: try:
print(f"👑 Админ запрашивает резюме: страница {page}, поиск '{search}'") print(f"👑 Админ {user_id} запрашивает список резюме")
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@@ -1791,19 +1848,9 @@ async def get_all_resumes_admin(
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone() user = cursor.fetchone()
if not user or not user["is_admin"]: if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
raise HTTPException(status_code=403, detail="Доступ запрещен") raise HTTPException(status_code=403, detail="Доступ запрещен")
# Базовый запрос для подсчета общего количества # Базовый запрос
count_query = """
SELECT COUNT(*)
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE 1=1
"""
count_params = []
# Базовый запрос для получения данных
query = """ query = """
SELECT SELECT
r.id, r.id,
@@ -1823,21 +1870,11 @@ async def get_all_resumes_admin(
""" """
params = [] params = []
# Добавляем поиск, если есть # Поиск
if search: if search:
query += " AND (u.full_name LIKE ? OR u.email LIKE ? OR r.desired_position LIKE ?)"
search_term = f"%{search}%" search_term = f"%{search}%"
search_condition = """ AND ( params.extend([search_term, search_term, search_term])
u.full_name LIKE ? OR
u.email LIKE ? OR
r.desired_position LIKE ? OR
r.about_me LIKE ?
)"""
query += search_condition
count_query += search_condition
search_params = [search_term, search_term, search_term, search_term]
params.extend(search_params)
count_params.extend(search_params)
# Сортировка # Сортировка
query += " ORDER BY r.updated_at DESC" query += " ORDER BY r.updated_at DESC"
@@ -1847,38 +1884,36 @@ async def get_all_resumes_admin(
query += " LIMIT ? OFFSET ?" query += " LIMIT ? OFFSET ?"
params.extend([limit, offset]) params.extend([limit, offset])
# Получаем общее количество
cursor.execute(count_query, count_params)
total = cursor.fetchone()[0]
# Получаем резюме
cursor.execute(query, params) cursor.execute(query, params)
resumes = cursor.fetchall() resumes = cursor.fetchall()
# Получаем общее количество
count_query = "SELECT COUNT(*) FROM resumes"
if search:
count_query += " WHERE user_id IN (SELECT id FROM users WHERE full_name LIKE ? OR email LIKE ?)"
cursor.execute(count_query, [search_term, search_term])
else:
cursor.execute(count_query)
total = cursor.fetchone()[0]
result = [] result = []
for r in resumes: for r in resumes:
resume_dict = dict(r) resume_dict = dict(r)
# Получаем теги для резюме # Получаем теги
cursor.execute(""" cursor.execute("""
SELECT t.name, t.category SELECT t.name FROM tags t
FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ? WHERE rt.resume_id = ?
""", (resume_dict["id"],)) """, (resume_dict["id"],))
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()] resume_dict["tags"] = [tag["name"] for tag in cursor.fetchall()]
# Получаем опыт работы # Получаем опыт работы
cursor.execute(""" cursor.execute("""
SELECT position, company, period, description SELECT position, company, period, description
FROM work_experience FROM work_experience
WHERE resume_id = ? WHERE resume_id = ?
ORDER BY ORDER BY period DESC
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume_dict["id"],)) """, (resume_dict["id"],))
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()] resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
@@ -1891,9 +1926,6 @@ async def get_all_resumes_admin(
""", (resume_dict["id"],)) """, (resume_dict["id"],))
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()] resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()]
# Количество опыта для быстрого отображения
resume_dict["experience_count"] = len(resume_dict["work_experience"])
result.append(resume_dict) result.append(resume_dict)
print(f"✅ Загружено {len(result)} резюме из {total}") print(f"✅ Загружено {len(result)} резюме из {total}")
@@ -1919,27 +1951,22 @@ async def get_resume_admin(
resume_id: int, resume_id: int,
user_id: int = Depends(get_current_user) user_id: int = Depends(get_current_user)
): ):
"""Получение детальной информации о резюме для админки""" """Получение резюме для админки (с полными данными)"""
try: try:
print(f"👑 Админ {user_id} запрашивает детали резюме {resume_id}") print(f"👑 Админ {user_id} запрашивает резюме {resume_id}")
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Проверка прав администратора # Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone() admin = cursor.fetchone()
if not user or not user["is_admin"]: if not admin or not admin["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен") raise HTTPException(status_code=403, detail="Доступ запрещен")
# Получаем основную информацию о резюме # Получаем резюме
cursor.execute(""" cursor.execute("""
SELECT SELECT r.*, u.full_name, u.email, u.phone, u.telegram
r.*,
u.full_name,
u.email,
u.phone,
u.telegram
FROM resumes r FROM resumes r
JOIN users u ON r.user_id = u.id JOIN users u ON r.user_id = u.id
WHERE r.id = ? WHERE r.id = ?
@@ -1949,43 +1976,40 @@ async def get_resume_admin(
if not resume: if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено") raise HTTPException(status_code=404, detail="Резюме не найдено")
resume_dict = dict(resume) resume_data = dict(resume)
# Получаем теги
cursor.execute("""
SELECT t.* FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_id,))
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
# Получаем опыт работы # Получаем опыт работы
cursor.execute(""" cursor.execute("""
SELECT * FROM work_experience SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ? WHERE resume_id = ?
ORDER BY ORDER BY period DESC
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume_id,)) """, (resume_id,))
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()] resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
# Получаем образование # Получаем образование
cursor.execute(""" cursor.execute("""
SELECT * FROM education SELECT institution, specialty, graduation_year
FROM education
WHERE resume_id = ? WHERE resume_id = ?
ORDER BY graduation_year DESC ORDER BY graduation_year DESC
""", (resume_id,)) """, (resume_id,))
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()] resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
return resume_dict # Получаем теги
cursor.execute("""
SELECT t.id, t.name, t.category FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_id,))
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()]
return resume_data
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
print(f"❌ Ошибка при загрузке деталей резюме {resume_id}: {e}") print(f"❌ Ошибка при загрузке резюме {resume_id}: {e}")
traceback.print_exc() traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}") raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@@ -2006,16 +2030,14 @@ async def delete_resume_admin(
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone() user = cursor.fetchone()
if not user or not user["is_admin"]: if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
raise HTTPException(status_code=403, detail="Доступ запрещен") raise HTTPException(status_code=403, detail="Доступ запрещен")
# Проверяем существование резюме # Проверяем существование резюме
cursor.execute("SELECT id FROM resumes WHERE id = ?", (resume_id,)) cursor.execute("SELECT id FROM resumes WHERE id = ?", (resume_id,))
resume = cursor.fetchone() if not cursor.fetchone():
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено") raise HTTPException(status_code=404, detail="Резюме не найдено")
# Удаляем связанные записи (каскадно должно работать, но на всякий случай) # Удаляем связанные записи
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
@@ -2042,7 +2064,6 @@ async def get_resume(resume_id: int):
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Увеличиваем счетчик просмотров
cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,)) cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,))
cursor.execute(""" cursor.execute("""
@@ -2058,17 +2079,12 @@ async def get_resume(resume_id: int):
resume_data = dict(resume) resume_data = dict(resume)
# Получаем опыт работы (ОБНОВЛЕНО - добавили поле description) # Получаем опыт работы
cursor.execute(""" cursor.execute("""
SELECT position, company, period, description SELECT position, company, period, description
FROM work_experience FROM work_experience
WHERE resume_id = ? WHERE resume_id = ?
ORDER BY ORDER BY period DESC
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume_id,)) """, (resume_id,))
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
@@ -2081,13 +2097,13 @@ async def get_resume(resume_id: int):
""", (resume_id,)) """, (resume_id,))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
# Получаем теги # Получаем теги - ВАЖНО: возвращаем массив строк
cursor.execute(""" cursor.execute("""
SELECT t.* FROM tags t SELECT t.name FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ? WHERE rt.resume_id = ?
""", (resume_id,)) """, (resume_id,))
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()] resume_data["tags"] = [tag["name"] for tag in cursor.fetchall()]
conn.commit() conn.commit()
return resume_data return resume_data
@@ -3201,19 +3217,169 @@ async def get_all_vacancies_admin(user_id: int = Depends(get_current_user)):
@app.delete("/api/admin/vacancies/{vacancy_id}") @app.delete("/api/admin/vacancies/{vacancy_id}")
async def delete_vacancy_admin(vacancy_id: int, user_id: int = Depends(get_current_user)): async def delete_vacancy_admin(
"""Удаление вакансии (для админа)""" vacancy_id: int,
user_id: int = Depends(get_current_user)
):
"""Удаление вакансии (только для админа)"""
try:
print(f"👑 Админ {user_id} пытается удалить вакансию {vacancy_id}")
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone() user = cursor.fetchone()
if not user or not user["is_admin"]: if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен") raise HTTPException(status_code=403, detail="Доступ запрещен")
# Проверяем существование вакансии
cursor.execute("SELECT id FROM vacancies WHERE id = ?", (vacancy_id,))
if not cursor.fetchone():
raise HTTPException(status_code=404, detail="Вакансия не найдена")
# Удаляем связанные теги
cursor.execute("DELETE FROM vacancy_tags WHERE vacancy_id = ?", (vacancy_id,))
# Удаляем отклики на эту вакансию
cursor.execute("DELETE FROM applications WHERE vacancy_id = ?", (vacancy_id,))
# Удаляем вакансию
cursor.execute("DELETE FROM vacancies WHERE id = ?", (vacancy_id,)) cursor.execute("DELETE FROM vacancies WHERE id = ?", (vacancy_id,))
conn.commit() conn.commit()
return {"message": "Вакансия удалена"} print(f"✅ Вакансия {vacancy_id} успешно удалена")
return {"message": "Вакансия успешно удалена"}
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при удалении вакансии {vacancy_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.put("/api/admin/resumes/{resume_id}")
async def update_resume_admin(
resume_id: int,
resume_update: ResumeCreate,
user_id: int = Depends(get_current_user)
):
"""Обновление резюме администратором (по ID резюме)"""
try:
print(f"👑 Админ {user_id} редактирует резюме {resume_id}")
with get_db() as conn:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
admin = cursor.fetchone()
if not admin or not admin["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Проверяем существование резюме
cursor.execute("SELECT user_id FROM resumes WHERE id = ?", (resume_id,))
resume = cursor.fetchone()
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
owner_id = resume["user_id"]
# Обновляем резюме для правильного владельца
cursor.execute("""
UPDATE resumes
SET desired_position = ?, about_me = ?, desired_salary = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""", (resume_update.desired_position, resume_update.about_me, resume_update.desired_salary, resume_id))
# Удаляем старые записи
cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
# Добавляем опыт работы
for exp in resume_update.work_experience:
cursor.execute("""
INSERT INTO work_experience (resume_id, position, company, period, description)
VALUES (?, ?, ?, ?, ?)
""", (resume_id, exp.position, exp.company, exp.period, exp.description))
# Добавляем образование
for edu in resume_update.education:
cursor.execute("""
INSERT INTO education (resume_id, institution, specialty, graduation_year)
VALUES (?, ?, ?, ?)
""", (resume_id, edu.institution, edu.specialty, edu.graduation_year))
# Добавляем теги
if resume_update.tags:
for tag_name in resume_update.tags:
if tag_name and tag_name.strip():
tag_name = tag_name.strip()
cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,))
tag = cursor.fetchone()
if tag:
tag_id = tag["id"]
else:
cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,))
tag_id = cursor.lastrowid
cursor.execute("""
INSERT OR IGNORE INTO resume_tags (resume_id, tag_id)
VALUES (?, ?)
""", (resume_id, tag_id))
conn.commit()
# Получаем обновленное резюме
cursor.execute("""
SELECT r.*, u.full_name, u.email, u.phone, u.telegram
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE r.id = ?
""", (resume_id,))
updated_resume = dict(cursor.fetchone())
# Получаем опыт работы
cursor.execute("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY period DESC
""", (resume_id,))
updated_resume["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
# Получаем образование
cursor.execute("""
SELECT institution, specialty, graduation_year
FROM education
WHERE resume_id = ?
ORDER BY graduation_year DESC
""", (resume_id,))
updated_resume["education"] = [dict(edu) for edu in cursor.fetchall()]
# Получаем теги
cursor.execute("""
SELECT t.id, t.name, t.category FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_id,))
updated_resume["tags"] = [dict(tag) for tag in cursor.fetchall()]
print(f"✅ Резюме {resume_id} обновлено администратором для пользователя {owner_id}")
return updated_resume
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при обновлении резюме {resume_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
# ========== ПОЛЬЗОВАТЕЛЬСКИЕ ЭНДПОИНТЫ ========== # ========== ПОЛЬЗОВАТЕЛЬСКИЕ ЭНДПОИНТЫ ==========

File diff suppressed because it is too large Load Diff

View File

@@ -1008,9 +1008,19 @@
async function loadResume() { async function loadResume() {
try { try {
const response = await fetch(`${API_BASE_URL}/resumes/${resumeId}`); const response = await fetch(`${API_BASE_URL}/resumes/${resumeId}`);
if (!response.ok) throw new Error('Резюме не найдено');
if (!response.ok) {
throw new Error('Резюме не найдено');
}
currentResume = await response.json(); currentResume = await response.json();
// Отладочный вывод
console.log('📥 Загружено резюме:', currentResume);
console.log('📌 Теги (сырые):', currentResume.tags);
renderResume(currentResume); renderResume(currentResume);
} catch (error) { } catch (error) {
console.error('Error loading resume:', error); console.error('Error loading resume:', error);
document.getElementById('resumeDetail').innerHTML = ` document.getElementById('resumeDetail').innerHTML = `
@@ -1028,19 +1038,46 @@
function renderResume(resume) { function renderResume(resume) {
const container = document.getElementById('resumeDetail'); const container = document.getElementById('resumeDetail');
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
const canContact = token && currentUser && currentUser.role === 'employer'; const canContact = token && currentUser && currentUser.role === 'employer';
const decodedName = decodeHtmlEntities(resume.full_name || ''); const decodedName = decodeHtmlEntities(resume.full_name || '');
const decodedPosition = decodeHtmlEntities(resume.desired_position || ''); const decodedPosition = decodeHtmlEntities(resume.desired_position || '');
// Проверяем и обрабатываем теги (поддержка разных форматов)
let tags = [];
if (resume.tags) {
if (Array.isArray(resume.tags)) {
// Если теги приходят как массив объектов или строк
tags = resume.tags.map(t => {
if (typeof t === 'string') return t;
if (t.name) return t.name;
return t;
});
} else if (typeof resume.tags === 'string') {
tags = resume.tags.split(',').map(t => t.trim()).filter(t => t);
}
}
console.log('📌 Теги для отображения:', tags);
const experienceHtml = resume.work_experience && resume.work_experience.length > 0 const experienceHtml = resume.work_experience && resume.work_experience.length > 0
? resume.work_experience.map(exp => ` ? resume.work_experience.map(exp => `
<div class="experience-item"> <div class="experience-item">
<div class="experience-header"> <div class="experience-header">
<span class="experience-title">${escapeHtml(exp.position)}</span> <span class="experience-title">${escapeHtml(exp.position)}</span>
<span class="experience-period"><i class="far fa-calendar"></i> ${escapeHtml(exp.period || 'Период не указан')}</span> <span class="experience-period">
<i class="far fa-calendar"></i> ${escapeHtml(exp.period || 'Период не указан')}
</span>
</div> </div>
<div class="experience-company"><i class="fas fa-building"></i> ${escapeHtml(exp.company)}</div> <div class="experience-company">
${exp.description ? `<div class="experience-description"><strong>📋 Обязанности и достижения:</strong><p style="margin-top: 10px;">${escapeHtml(exp.description).replace(/\n/g, '<br>')}</p></div>` : ''} <i class="fas fa-building"></i> ${escapeHtml(exp.company)}
</div>
${exp.description ? `
<div class="experience-description">
<strong>📋 Обязанности и достижения:</strong>
<p style="margin-top: 10px;">${escapeHtml(exp.description).replace(/\n/g, '<br>')}</p>
</div>
` : ''}
</div> </div>
`).join('') `).join('')
: '<p style="color: #4f7092; text-align: center; padding: 20px;">Опыт работы не указан</p>'; : '<p style="color: #4f7092; text-align: center; padding: 20px;">Опыт работы не указан</p>';
@@ -1059,7 +1096,9 @@
container.innerHTML = ` container.innerHTML = `
<div class="resume-header"> <div class="resume-header">
<div class="resume-avatar"><i class="fas fa-user"></i></div> <div class="resume-avatar">
<i class="fas fa-user"></i>
</div>
<div class="resume-title"> <div class="resume-title">
<div class="resume-title-header"> <div class="resume-title-header">
<div class="resume-name">${escapeHtml(decodedName)}</div> <div class="resume-name">${escapeHtml(decodedName)}</div>
@@ -1070,7 +1109,9 @@
</div> </div>
<div class="resume-position">${escapeHtml(decodedPosition)}</div> <div class="resume-position">${escapeHtml(decodedPosition)}</div>
<div class="resume-stats"> <div class="resume-stats">
<span class="view-counter"><i class="fas fa-eye"></i> ${resume.views || 0} просмотров</span> <span class="view-counter">
<i class="fas fa-eye"></i> ${resume.views || 0} просмотров
</span>
<span><i class="fas fa-calendar"></i> Обновлено: ${new Date(resume.updated_at).toLocaleDateString()}</span> <span><i class="fas fa-calendar"></i> Обновлено: ${new Date(resume.updated_at).toLocaleDateString()}</span>
</div> </div>
</div> </div>
@@ -1078,8 +1119,11 @@
<div class="resume-salary">${escapeHtml(resume.desired_salary || 'Зарплатные ожидания не указаны')}</div> <div class="resume-salary">${escapeHtml(resume.desired_salary || 'Зарплатные ожидания не указаны')}</div>
${resume.tags && resume.tags.length > 0 ? ` <!-- Блок с тегами - исправленное отображение -->
<div class="resume-tags">${resume.tags.map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}</div> ${tags && tags.length > 0 ? `
<div class="resume-tags">
${tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
</div>
` : ''} ` : ''}
<h2 class="section-title">О себе</h2> <h2 class="section-title">О себе</h2>
@@ -1093,25 +1137,50 @@
<div class="contact-section"> <div class="contact-section">
<h3 style="color: #0b1c34; margin-bottom: 20px;">Контактная информация</h3> <h3 style="color: #0b1c34; margin-bottom: 20px;">Контактная информация</h3>
${canContact ? ` ${canContact ? `
<div class="contact-grid"> <div class="contact-grid">
<div class="contact-item"><i class="fas fa-envelope"></i><span>${escapeHtml(resume.email || 'Email не указан')}</span></div> <div class="contact-item">
<div class="contact-item"><i class="fas fa-phone"></i><span>${escapeHtml(resume.phone || 'Телефон не указан')}</span></div> <i class="fas fa-envelope"></i>
<div class="contact-item"><i class="fab fa-telegram"></i><span>${escapeHtml(resume.telegram || 'Telegram не указан')}</span></div> <span>${escapeHtml(resume.email || 'Email не указан')}</span>
</div>
<div class="contact-item">
<i class="fas fa-phone"></i>
<span>${escapeHtml(resume.phone || 'Телефон не указан')}</span>
</div>
<div class="contact-item">
<i class="fab fa-telegram"></i>
<span>${escapeHtml(resume.telegram || 'Telegram не указан')}</span>
</div>
</div> </div>
` : ` ` : `
<div style="text-align: center; padding: 30px; background: white; border-radius: 20px;"> <div style="text-align: center; padding: 30px; background: white; border-radius: 20px;">
<i class="fas fa-lock" style="font-size: 48px; color: #4f7092; margin-bottom: 15px;"></i> <i class="fas fa-lock" style="font-size: 48px; color: #4f7092; margin-bottom: 15px;"></i>
<p style="color: #1f3f60; margin-bottom: 20px;">Контактные данные доступны только авторизованным работодателям</p> <p style="color: #1f3f60; margin-bottom: 20px;">
${!token ? '<a href="/login" class="btn btn-primary">Войти как работодатель</a>' : '<p class="btn btn-primary" style="opacity: 0.6;">Только для работодателей</p>'} Контактные данные доступны только авторизованным работодателям
</p>
${!token ?
'<a href="/login" class="btn btn-primary">Войти как работодатель</a>' :
'<p class="btn btn-primary" style="opacity: 0.6;">Только для работодателей</p>'
}
</div> </div>
`} `}
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
${canContact ? `<button class="btn btn-primary" onclick="contactCandidate()"><i class="fas fa-paper-plane"></i> Связаться</button>` : ''} ${canContact ? `
<button class="btn btn-outline" onclick="saveToFavorites()"><i class="fas fa-heart"></i> В избранное</button> <button class="btn btn-primary" onclick="contactCandidate()">
<button class="btn btn-outline" onclick="shareResume()"><i class="fas fa-share-alt"></i> Поделиться</button> <i class="fas fa-paper-plane"></i> Связаться
</button>
` : ''}
<button class="btn btn-outline" onclick="saveToFavorites()">
<i class="fas fa-heart"></i> В избранное
</button>
<button class="btn btn-outline" onclick="shareResume()">
<i class="fas fa-share-alt"></i> Поделиться
</button>
</div> </div>
`; `;
} }