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

712
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,99 +1427,121 @@ async def update_vacancy(
vacancy_update: VacancyUpdate, vacancy_update: VacancyUpdate,
user_id: int = Depends(get_current_user) user_id: int = Depends(get_current_user)
): ):
"""Обновление существующей вакансии""" """Обновление существующей вакансии (для админа и владельца)"""
with get_db() as conn: try:
cursor = conn.cursor() print(f"📝 Обновление вакансии {vacancy_id} пользователем {user_id}")
# Проверяем, что вакансия принадлежит пользователю with get_db() as conn:
cursor.execute(""" cursor = conn.cursor()
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() # Проверяем существование вакансии
if not vacancy: cursor.execute("SELECT * FROM vacancies WHERE id = ?", (vacancy_id,))
raise HTTPException(status_code=404, detail="Вакансия не найдена") vacancy = cursor.fetchone()
# Проверяем роль (только работодатель может редактировать) if not vacancy:
if vacancy["role"] != "employer": raise HTTPException(status_code=404, detail="Вакансия не найдена")
raise HTTPException(status_code=403, detail="Только работодатели могут редактировать вакансии")
# Обновляем основные поля # Проверяем права (админ или владелец)
update_fields = [] cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
params = [] current_user = cursor.fetchone()
is_admin = current_user and current_user["is_admin"] == 1
if vacancy_update.title is not None: # Если не админ и не владелец - запрещаем
update_fields.append("title = ?") if not is_admin and vacancy["user_id"] != user_id:
params.append(vacancy_update.title) print(
f"❌ Доступ запрещен: пользователь {user_id} не является админом или владельцем вакансии {vacancy_id}")
raise HTTPException(status_code=403, detail="Нет прав на редактирование")
if vacancy_update.salary is not None: print(f"✅ Права проверены: is_admin={is_admin}, is_owner={vacancy['user_id'] == user_id}")
update_fields.append("salary = ?")
params.append(vacancy_update.salary)
if vacancy_update.description is not None: # Обновляем основные поля
update_fields.append("description = ?") update_fields = []
params.append(vacancy_update.description) params = []
if vacancy_update.contact is not None: if vacancy_update.title is not None:
update_fields.append("contact = ?") update_fields.append("title = ?")
params.append(vacancy_update.contact) params.append(vacancy_update.title)
if vacancy_update.is_active is not None: if vacancy_update.salary is not None:
update_fields.append("is_active = ?") update_fields.append("salary = ?")
params.append(1 if vacancy_update.is_active else 0) params.append(vacancy_update.salary)
if update_fields: if vacancy_update.description is not None:
query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?" update_fields.append("description = ?")
params.append(vacancy_id) params.append(vacancy_update.description)
cursor.execute(query, params)
# Обновляем теги if vacancy_update.contact is not None:
if vacancy_update.tags is not None: update_fields.append("contact = ?")
# Удаляем старые теги params.append(vacancy_update.contact)
cursor.execute("DELETE FROM vacancy_tags WHERE vacancy_id = ?", (vacancy_id,))
# Добавляем новые теги if vacancy_update.is_active is not None:
for tag_name in vacancy_update.tags: update_fields.append("is_active = ?")
# Ищем или создаем тег params.append(1 if vacancy_update.is_active else 0)
cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,))
tag = cursor.fetchone()
if tag: if update_fields:
tag_id = tag["id"] query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?"
else: params.append(vacancy_id)
cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,)) cursor.execute(query, params)
tag_id = cursor.lastrowid print(f"✅ Основные поля вакансии {vacancy_id} обновлены")
cursor.execute(""" # Обновляем теги
INSERT INTO vacancy_tags (vacancy_id, tag_id) if vacancy_update.tags is not None:
VALUES (?, ?) # Удаляем старые теги
""", (vacancy_id, tag_id)) cursor.execute("DELETE FROM vacancy_tags WHERE vacancy_id = ?", (vacancy_id,))
conn.commit() # Добавляем новые теги
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,))
tag = cursor.fetchone()
# Возвращаем обновленную вакансию if tag:
cursor.execute(""" tag_id = tag["id"]
SELECT v.*, COALESCE(c.name, u.full_name) as company_name else:
FROM vacancies v cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,))
JOIN users u ON v.user_id = u.id tag_id = cursor.lastrowid
LEFT JOIN companies c ON v.user_id = c.user_id
WHERE v.id = ?
""", (vacancy_id,))
updated_vacancy = dict(cursor.fetchone()) cursor.execute("""
INSERT INTO vacancy_tags (vacancy_id, tag_id)
VALUES (?, ?)
""", (vacancy_id, tag_id))
# Добавляем теги print(f"✅ Теги вакансии {vacancy_id} обновлены")
cursor.execute("""
SELECT t.* FROM tags t
JOIN vacancy_tags vt ON t.id = vt.tag_id
WHERE vt.vacancy_id = ?
""", (vacancy_id,))
updated_vacancy["tags"] = [dict(tag) for tag in cursor.fetchall()]
return updated_vacancy conn.commit()
# Возвращаем обновленную вакансию
cursor.execute("""
SELECT v.*,
COALESCE(c.name, u.full_name) as company_name,
u.full_name as user_name
FROM vacancies v
JOIN users u ON v.user_id = u.id
LEFT JOIN companies c ON v.user_id = c.user_id
WHERE v.id = ?
""", (vacancy_id,))
updated_vacancy = dict(cursor.fetchone())
# Добавляем теги
cursor.execute("""
SELECT t.name FROM tags t
JOIN vacancy_tags vt ON t.id = vt.tag_id
WHERE vt.vacancy_id = ?
""", (vacancy_id,))
updated_vacancy["tags"] = [tag["name"] for tag in cursor.fetchall()]
print(f"✅ Вакансия {vacancy_id} успешно обновлена")
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")
@@ -1558,102 +1581,136 @@ async def create_or_update_resume(
resume: ResumeCreate, resume: ResumeCreate,
user_id: int = Depends(get_current_user) user_id: int = Depends(get_current_user)
): ):
"""Создание или обновление резюме""" """Создание или обновление резюме (для админа и владельца)"""
with get_db() as conn: try:
cursor = conn.cursor() print(f"📝 Сохранение резюме для пользователя {user_id}")
# Проверка роли with get_db() as conn:
cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) cursor = conn.cursor()
user = cursor.fetchone()
if not user or user["role"] != "employee":
raise HTTPException(status_code=403, detail="Только соискатели могут создавать резюме")
# Проверка существования резюме # Получаем информацию о текущем пользователе
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,)) cursor.execute("SELECT role, is_admin FROM users WHERE id = ?", (user_id,))
existing = cursor.fetchone() current_user = cursor.fetchone()
if existing: if not current_user:
# Обновление существующего резюме raise HTTPException(status_code=404, detail="Пользователь не найден")
cursor.execute("""
UPDATE resumes
SET desired_position = ?, about_me = ?, desired_salary = ?, updated_at = CURRENT_TIMESTAMP
WHERE user_id = ?
""", (resume.desired_position, resume.about_me, resume.desired_salary, user_id))
resume_id = existing["id"] is_admin = current_user["is_admin"] == 1
is_employee = current_user["role"] == "employee"
# Удаление старых записей # Если пользователь не админ и не соискатель
cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,)) if not is_admin and not is_employee:
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,)) raise HTTPException(status_code=403, detail="Только соискатели и администраторы могут создавать резюме")
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
else:
# Создание нового резюме
cursor.execute("""
INSERT INTO resumes (user_id, desired_position, about_me, desired_salary)
VALUES (?, ?, ?, ?)
""", (user_id, resume.desired_position, resume.about_me, resume.desired_salary))
resume_id = cursor.lastrowid # Проверяем существование резюме
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,))
# Добавление опыта работы (ОБНОВЛЕНО - добавили description) existing = cursor.fetchone()
for exp in resume.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.education:
cursor.execute("""
INSERT INTO education (resume_id, institution, specialty, graduation_year)
VALUES (?, ?, ?, ?)
""", (resume_id, edu.institution, edu.specialty, edu.graduation_year))
# Добавление тегов
if resume.tags:
for tag_name in resume.tags:
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
if existing:
# Обновление существующего резюме
cursor.execute(""" cursor.execute("""
INSERT OR IGNORE INTO resume_tags (resume_id, tag_id) UPDATE resumes
VALUES (?, ?) SET desired_position = ?, about_me = ?, desired_salary = ?, updated_at = CURRENT_TIMESTAMP
""", (resume_id, tag_id)) WHERE user_id = ?
""", (resume.desired_position, resume.about_me, resume.desired_salary, user_id))
conn.commit() resume_id = existing["id"]
# Получение обновленного резюме # Удаление старых записей
cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,)) cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,))
resume_data = dict(cursor.fetchone()) cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
cursor.execute(""" print(f"✅ Обновлено существующее резюме ID {resume_id} для пользователя {user_id}")
SELECT position, company, period, description else:
FROM work_experience # Создание нового резюме
WHERE resume_id = ? cursor.execute("""
""", (resume_id,)) INSERT INTO resumes (user_id, desired_position, about_me, desired_salary)
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] VALUES (?, ?, ?, ?)
""", (user_id, resume.desired_position, resume.about_me, resume.desired_salary))
cursor.execute(""" resume_id = cursor.lastrowid
SELECT institution, specialty, graduation_year print(f"✅ Создано новое резюме ID {resume_id} для пользователя {user_id}")
FROM education
WHERE resume_id = ?
""", (resume_id,))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
cursor.execute(""" # Добавление опыта работы
SELECT t.* FROM tags t for exp in resume.work_experience:
JOIN resume_tags rt ON t.id = rt.tag_id cursor.execute("""
WHERE rt.resume_id = ? INSERT INTO work_experience (resume_id, position, company, period, description)
""", (resume_id,)) VALUES (?, ?, ?, ?, ?)
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()] """, (resume_id, exp.position, exp.company, exp.period, exp.description))
return resume_data # Добавление образования
for edu in resume.education:
cursor.execute("""
INSERT INTO education (resume_id, institution, specialty, graduation_year)
VALUES (?, ?, ?, ?)
""", (resume_id, edu.institution, edu.specialty, edu.graduation_year))
# Добавление тегов
if 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,))
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 * FROM resumes WHERE id = ?", (resume_id,))
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("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY period DESC
""", (resume_id,))
resume_data["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,))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
# ВАЖНО: теги возвращаем в формате списка объектов с полями id, name, category
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()]
print(f"✅ Резюме {resume_id} успешно сохранено")
print(f"📌 Теги в ответе: {resume_data['tags']}")
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)
@@ -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,
with get_db() as conn: user_id: int = Depends(get_current_user)
cursor = conn.cursor() ):
"""Удаление вакансии (только для админа)"""
try:
print(f"👑 Админ {user_id} пытается удалить вакансию {vacancy_id}")
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) with get_db() as conn:
user = cursor.fetchone() cursor = conn.cursor()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
cursor.execute("DELETE FROM vacancies WHERE id = ?", (vacancy_id,)) # Проверка прав администратора
conn.commit() cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
return {"message": "Вакансия удалена"} user = cursor.fetchone()
if not user or not user["is_admin"]:
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,))
conn.commit()
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>
`; `;
} }