admin resume and vacancy edit
This commit is contained in:
712
server.py
712
server.py
@@ -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
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user