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.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,25 +1427,32 @@ async def update_vacancy(
vacancy_update: VacancyUpdate,
user_id: int = Depends(get_current_user)
):
"""Обновление существующей вакансии"""
"""Обновление существующей вакансии (для админа и владельца)"""
try:
print(f"📝 Обновление вакансии {vacancy_id} пользователем {user_id}")
with get_db() as conn:
cursor = conn.cursor()
# Проверяем, что вакансия принадлежит пользователю
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))
# Проверяем существование вакансии
cursor.execute("SELECT * FROM vacancies WHERE id = ?", (vacancy_id,))
vacancy = cursor.fetchone()
if not vacancy:
raise HTTPException(status_code=404, detail="Вакансия не найдена")
# Проверяем роль (только работодатель может редактировать)
if vacancy["role"] != "employer":
raise HTTPException(status_code=403, detail="Только работодатели могут редактировать вакансии")
# Проверяем права (админ или владелец)
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 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 = []
@@ -1474,6 +1482,7 @@ async def update_vacancy(
query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?"
params.append(vacancy_id)
cursor.execute(query, params)
print(f"✅ Основные поля вакансии {vacancy_id} обновлены")
# Обновляем теги
if vacancy_update.tags is not None:
@@ -1482,6 +1491,8 @@ async def update_vacancy(
# Добавляем новые теги
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()
@@ -1497,11 +1508,15 @@ async def update_vacancy(
VALUES (?, ?)
""", (vacancy_id, tag_id))
print(f"✅ Теги вакансии {vacancy_id} обновлены")
conn.commit()
# Возвращаем обновленную вакансию
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
JOIN users u ON v.user_id = u.id
LEFT JOIN companies c ON v.user_id = c.user_id
@@ -1512,14 +1527,22 @@ async def update_vacancy(
# Добавляем теги
cursor.execute("""
SELECT t.* FROM tags t
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"] = [dict(tag) for tag in cursor.fetchall()]
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")
async def apply_to_vacancy(
@@ -1558,17 +1581,28 @@ async def create_or_update_resume(
resume: ResumeCreate,
user_id: int = Depends(get_current_user)
):
"""Создание или обновление резюме"""
"""Создание или обновление резюме (для админа и владельца)"""
try:
print(f"📝 Сохранение резюме для пользователя {user_id}")
with get_db() as conn:
cursor = conn.cursor()
# Проверка роли
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="Только соискатели могут создавать резюме")
# Получаем информацию о текущем пользователе
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="Пользователь не найден")
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,))
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 education WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
print(f"✅ Обновлено существующее резюме ID {resume_id} для пользователя {user_id}")
else:
# Создание нового резюме
cursor.execute("""
@@ -1594,8 +1630,9 @@ async def create_or_update_resume(
""", (user_id, resume.desired_position, resume.about_me, resume.desired_salary))
resume_id = cursor.lastrowid
print(f"✅ Создано новое резюме ID {resume_id} для пользователя {user_id}")
# Добавление опыта работы (ОБНОВЛЕНО - добавили description)
# Добавление опыта работы
for exp in resume.work_experience:
cursor.execute("""
INSERT INTO work_experience (resume_id, position, company, period, description)
@@ -1612,6 +1649,9 @@ async def create_or_update_resume(
# Добавление тегов
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()
@@ -1628,14 +1668,20 @@ async def create_or_update_resume(
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()]
@@ -1643,18 +1689,29 @@ async def create_or_update_resume(
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.* FROM tags t
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)
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:
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)):
"""Удаление вакансии (для админа)"""
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:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Проверяем существование вакансии
cursor.execute("SELECT 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()
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() {
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>
`;
}