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