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.responses import HTMLResponse, FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, EmailStr, ConfigDict
from typing import Optional, List
from datetime import datetime, timedelta, timezone
from jose import jwt
@@ -318,9 +318,10 @@ class TokenResponse(BaseModel):
class Tag(BaseModel):
id: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
id: int
name: str
category: str = "skill"
category: Optional[str] = None
class VacancyCreate(BaseModel):
@@ -383,7 +384,7 @@ class ResumeResponse(BaseModel):
desired_salary: Optional[str]
work_experience: List[WorkExperience]
education: List[Education]
tags: List[Tag] = []
tags: List[Tag]
views: int
updated_at: str
full_name: Optional[str] = None
@@ -1426,99 +1427,121 @@ async def update_vacancy(
vacancy_update: VacancyUpdate,
user_id: int = Depends(get_current_user)
):
"""Обновление существующей вакансии"""
with get_db() as conn:
cursor = conn.cursor()
"""Обновление существующей вакансии (для админа и владельца)"""
try:
print(f"📝 Обновление вакансии {vacancy_id} пользователем {user_id}")
# Проверяем, что вакансия принадлежит пользователю
cursor.execute("""
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))
with get_db() as conn:
cursor = conn.cursor()
vacancy = cursor.fetchone()
if not vacancy:
raise HTTPException(status_code=404, detail="Вакансия не найдена")
# Проверяем существование вакансии
cursor.execute("SELECT * FROM vacancies WHERE id = ?", (vacancy_id,))
vacancy = cursor.fetchone()
# Проверяем роль (только работодатель может редактировать)
if vacancy["role"] != "employer":
raise HTTPException(status_code=403, detail="Только работодатели могут редактировать вакансии")
if not vacancy:
raise HTTPException(status_code=404, detail="Вакансия не найдена")
# Обновляем основные поля
update_fields = []
params = []
# Проверяем права (админ или владелец)
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
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 = ?")
params.append(vacancy_update.title)
# Если не админ и не владелец - запрещаем
if not is_admin and vacancy["user_id"] != user_id:
print(
f"❌ Доступ запрещен: пользователь {user_id} не является админом или владельцем вакансии {vacancy_id}")
raise HTTPException(status_code=403, detail="Нет прав на редактирование")
if vacancy_update.salary is not None:
update_fields.append("salary = ?")
params.append(vacancy_update.salary)
print(f"✅ Права проверены: is_admin={is_admin}, is_owner={vacancy['user_id'] == user_id}")
if vacancy_update.description is not None:
update_fields.append("description = ?")
params.append(vacancy_update.description)
# Обновляем основные поля
update_fields = []
params = []
if vacancy_update.contact is not None:
update_fields.append("contact = ?")
params.append(vacancy_update.contact)
if vacancy_update.title is not None:
update_fields.append("title = ?")
params.append(vacancy_update.title)
if vacancy_update.is_active is not None:
update_fields.append("is_active = ?")
params.append(1 if vacancy_update.is_active else 0)
if vacancy_update.salary is not None:
update_fields.append("salary = ?")
params.append(vacancy_update.salary)
if update_fields:
query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?"
params.append(vacancy_id)
cursor.execute(query, params)
if vacancy_update.description is not None:
update_fields.append("description = ?")
params.append(vacancy_update.description)
# Обновляем теги
if vacancy_update.tags is not None:
# Удаляем старые теги
cursor.execute("DELETE FROM vacancy_tags WHERE vacancy_id = ?", (vacancy_id,))
if vacancy_update.contact is not None:
update_fields.append("contact = ?")
params.append(vacancy_update.contact)
# Добавляем новые теги
for tag_name in vacancy_update.tags:
# Ищем или создаем тег
cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,))
tag = cursor.fetchone()
if vacancy_update.is_active is not None:
update_fields.append("is_active = ?")
params.append(1 if vacancy_update.is_active else 0)
if tag:
tag_id = tag["id"]
else:
cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,))
tag_id = cursor.lastrowid
if update_fields:
query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?"
params.append(vacancy_id)
cursor.execute(query, params)
print(f"✅ Основные поля вакансии {vacancy_id} обновлены")
cursor.execute("""
INSERT INTO vacancy_tags (vacancy_id, tag_id)
VALUES (?, ?)
""", (vacancy_id, tag_id))
# Обновляем теги
if vacancy_update.tags is not None:
# Удаляем старые теги
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()
# Возвращаем обновленную вакансию
cursor.execute("""
SELECT v.*, COALESCE(c.name, u.full_name) as company_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,))
if tag:
tag_id = tag["id"]
else:
cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,))
tag_id = cursor.lastrowid
updated_vacancy = dict(cursor.fetchone())
cursor.execute("""
INSERT INTO vacancy_tags (vacancy_id, tag_id)
VALUES (?, ?)
""", (vacancy_id, tag_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()]
print(f"✅ Теги вакансии {vacancy_id} обновлены")
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")
@@ -1558,102 +1581,136 @@ async def create_or_update_resume(
resume: ResumeCreate,
user_id: int = Depends(get_current_user)
):
"""Создание или обновление резюме"""
with get_db() as conn:
cursor = conn.cursor()
"""Создание или обновление резюме (для админа и владельца)"""
try:
print(f"📝 Сохранение резюме для пользователя {user_id}")
# Проверка роли
cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or user["role"] != "employee":
raise HTTPException(status_code=403, detail="Только соискатели могут создавать резюме")
with get_db() as conn:
cursor = conn.cursor()
# Проверка существования резюме
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,))
existing = cursor.fetchone()
# Получаем информацию о текущем пользователе
cursor.execute("SELECT role, is_admin FROM users WHERE id = ?", (user_id,))
current_user = cursor.fetchone()
if existing:
# Обновление существующего резюме
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))
if not current_user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
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,))
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
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))
# Если пользователь не админ и не соискатель
if not is_admin and not is_employee:
raise HTTPException(status_code=403, detail="Только соискатели и администраторы могут создавать резюме")
resume_id = cursor.lastrowid
# Добавление опыта работы (ОБНОВЛЕНО - добавили description)
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
# Проверяем существование резюме
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,))
existing = cursor.fetchone()
if existing:
# Обновление существующего резюме
cursor.execute("""
INSERT OR IGNORE INTO resume_tags (resume_id, tag_id)
VALUES (?, ?)
""", (resume_id, tag_id))
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))
conn.commit()
resume_id = existing["id"]
# Получение обновленного резюме
cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,))
resume_data = dict(cursor.fetchone())
# Удаление старых записей
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,))
cursor.execute("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
""", (resume_id,))
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
print(f"✅ Обновлено существующее резюме ID {resume_id} для пользователя {user_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))
cursor.execute("""
SELECT institution, specialty, graduation_year
FROM education
WHERE resume_id = ?
""", (resume_id,))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
resume_id = cursor.lastrowid
print(f"✅ Создано новое резюме ID {resume_id} для пользователя {user_id}")
cursor.execute("""
SELECT t.* 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()]
# Добавление опыта работы
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))
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)
@@ -1662,6 +1719,14 @@ async def get_my_resume(user_id: int = Depends(get_current_user)):
with get_db() as conn:
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,))
resume = cursor.fetchone()
@@ -1670,21 +1735,14 @@ async def get_my_resume(user_id: int = Depends(get_current_user)):
resume_data = dict(resume)
# Получаем опыт работы (ОБНОВЛЕНО - добавили поле description)
cursor.execute("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
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
@@ -1693,13 +1751,12 @@ async def get_my_resume(user_id: int = Depends(get_current_user)):
""", (resume["id"],))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
# Получаем теги
cursor.execute("""
SELECT t.* FROM tags t
SELECT t.name 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()]
resume_data["tags"] = [tag["name"] for tag in cursor.fetchall()]
return resume_data
@@ -1780,9 +1837,9 @@ async def get_all_resumes_admin(
limit: int = 10,
search: str = None
):
"""Получение всех резюме для админки с пагинацией и поиском"""
"""Получение всех резюме для админки"""
try:
print(f"👑 Админ запрашивает резюме: страница {page}, поиск '{search}'")
print(f"👑 Админ {user_id} запрашивает список резюме")
with get_db() as conn:
cursor = conn.cursor()
@@ -1791,19 +1848,9 @@ async def get_all_resumes_admin(
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
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 = """
SELECT
r.id,
@@ -1823,21 +1870,11 @@ async def get_all_resumes_admin(
"""
params = []
# Добавляем поиск, если есть
# Поиск
if search:
query += " AND (u.full_name LIKE ? OR u.email LIKE ? OR r.desired_position LIKE ?)"
search_term = f"%{search}%"
search_condition = """ AND (
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)
params.extend([search_term, search_term, search_term])
# Сортировка
query += " ORDER BY r.updated_at DESC"
@@ -1847,38 +1884,36 @@ async def get_all_resumes_admin(
query += " LIMIT ? OFFSET ?"
params.extend([limit, offset])
# Получаем общее количество
cursor.execute(count_query, count_params)
total = cursor.fetchone()[0]
# Получаем резюме
cursor.execute(query, params)
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 = []
for r in resumes:
resume_dict = dict(r)
# Получаем теги для резюме
# Получаем теги
cursor.execute("""
SELECT t.name, t.category
FROM tags t
SELECT t.name FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_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("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
ORDER BY period DESC
""", (resume_dict["id"],))
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["education"] = [dict(edu) for edu in cursor.fetchall()]
# Количество опыта для быстрого отображения
resume_dict["experience_count"] = len(resume_dict["work_experience"])
result.append(resume_dict)
print(f"✅ Загружено {len(result)} резюме из {total}")
@@ -1919,27 +1951,22 @@ async def get_resume_admin(
resume_id: int,
user_id: int = Depends(get_current_user)
):
"""Получение детальной информации о резюме для админки"""
"""Получение резюме для админки (с полными данными)"""
try:
print(f"👑 Админ {user_id} запрашивает детали резюме {resume_id}")
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,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
admin = cursor.fetchone()
if not admin or not admin["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Получаем основную информацию о резюме
# Получаем резюме
cursor.execute("""
SELECT
r.*,
u.full_name,
u.email,
u.phone,
u.telegram
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 = ?
@@ -1949,43 +1976,40 @@ async def get_resume_admin(
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
resume_dict = 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()]
resume_data = dict(resume)
# Получаем опыт работы
cursor.execute("""
SELECT * FROM work_experience
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
ORDER BY period DESC
""", (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("""
SELECT * FROM education
SELECT institution, specialty, graduation_year
FROM education
WHERE resume_id = ?
ORDER BY graduation_year DESC
""", (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:
raise
except Exception as e:
print(f"❌ Ошибка при загрузке деталей резюме {resume_id}: {e}")
print(f"❌ Ошибка при загрузке резюме {resume_id}: {e}")
traceback.print_exc()
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,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Проверяем существование резюме
cursor.execute("SELECT id FROM resumes WHERE id = ?", (resume_id,))
resume = cursor.fetchone()
if not resume:
if not cursor.fetchone():
raise HTTPException(status_code=404, detail="Резюме не найдено")
# Удаляем связанные записи (каскадно должно работать, но на всякий случай)
# Удаляем связанные записи
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 education WHERE resume_id = ?", (resume_id,))
@@ -2042,7 +2064,6 @@ async def get_resume(resume_id: int):
with get_db() as conn:
cursor = conn.cursor()
# Увеличиваем счетчик просмотров
cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,))
cursor.execute("""
@@ -2058,17 +2079,12 @@ async def get_resume(resume_id: int):
resume_data = dict(resume)
# Получаем опыт работы (ОБНОВЛЕНО - добавили поле description)
# Получаем опыт работы
cursor.execute("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
ORDER BY period DESC
""", (resume_id,))
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_data["education"] = [dict(edu) for edu in cursor.fetchall()]
# Получаем теги
# Получаем теги - ВАЖНО: возвращаем массив строк
cursor.execute("""
SELECT t.* FROM tags t
SELECT t.name 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()]
resume_data["tags"] = [tag["name"] for tag in cursor.fetchall()]
conn.commit()
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}")
async def delete_vacancy_admin(vacancy_id: int, user_id: int = Depends(get_current_user)):
"""Удаление вакансии (для админа)"""
with get_db() as conn:
cursor = conn.cursor()
async def delete_vacancy_admin(
vacancy_id: int,
user_id: int = Depends(get_current_user)
):
"""Удаление вакансии (только для админа)"""
try:
print(f"👑 Админ {user_id} пытается удалить вакансию {vacancy_id}")
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="Доступ запрещен")
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM vacancies WHERE id = ?", (vacancy_id,))
conn.commit()
return {"message": "Вакансия удалена"}
# Проверка прав администратора
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 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() {
try {
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();
// Отладочный вывод
console.log('📥 Загружено резюме:', currentResume);
console.log('📌 Теги (сырые):', currentResume.tags);
renderResume(currentResume);
} catch (error) {
console.error('Error loading resume:', error);
document.getElementById('resumeDetail').innerHTML = `
@@ -1028,19 +1038,46 @@
function renderResume(resume) {
const container = document.getElementById('resumeDetail');
const token = localStorage.getItem('accessToken');
const canContact = token && currentUser && currentUser.role === 'employer';
const decodedName = decodeHtmlEntities(resume.full_name || '');
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
? resume.work_experience.map(exp => `
<div class="experience-item">
<div class="experience-header">
<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 class="experience-company"><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 class="experience-company">
<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>
`).join('')
: '<p style="color: #4f7092; text-align: center; padding: 20px;">Опыт работы не указан</p>';
@@ -1059,7 +1096,9 @@
container.innerHTML = `
<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-header">
<div class="resume-name">${escapeHtml(decodedName)}</div>
@@ -1070,7 +1109,9 @@
</div>
<div class="resume-position">${escapeHtml(decodedPosition)}</div>
<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>
</div>
</div>
@@ -1078,8 +1119,11 @@
<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>
@@ -1093,25 +1137,50 @@
<div class="contact-section">
<h3 style="color: #0b1c34; margin-bottom: 20px;">Контактная информация</h3>
${canContact ? `
<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"><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 class="contact-item">
<i class="fas fa-envelope"></i>
<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 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>
<p style="color: #1f3f60; margin-bottom: 20px;">Контактные данные доступны только авторизованным работодателям</p>
${!token ? '<a href="/login" class="btn btn-primary">Войти как работодатель</a>' : '<p class="btn btn-primary" style="opacity: 0.6;">Только для работодателей</p>'}
<p style="color: #1f3f60; margin-bottom: 20px;">
Контактные данные доступны только авторизованным работодателям
</p>
${!token ?
'<a href="/login" class="btn btn-primary">Войти как работодатель</a>' :
'<p class="btn btn-primary" style="opacity: 0.6;">Только для работодателей</p>'
}
</div>
`}
</div>
<div class="action-buttons">
${canContact ? `<button class="btn btn-primary" onclick="contactCandidate()"><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>
${canContact ? `
<button class="btn btn-primary" onclick="contactCandidate()">
<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>
`;
}