diff --git a/server.py b/server.py index e20a35f..96aebb6 100644 --- a/server.py +++ b/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)}") # ========== ПОЛЬЗОВАТЕЛЬСКИЕ ЭНДПОИНТЫ ========== diff --git a/templates/admin.html b/templates/admin.html index 59b1f81..2175eb6 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -462,6 +462,172 @@ grid-template-columns: 1fr; } } + + /* Стили для форм редактирования */ + .edit-form { + margin-top: 20px; + } + + .form-group { + margin-bottom: 20px; + } + + .form-group label { + display: block; + font-weight: 600; + color: #1f3f60; + margin-bottom: 8px; + } + + .form-group input, + .form-group textarea, + .form-group select { + width: 100%; + padding: 12px 16px; + border: 2px solid #dee9f5; + border-radius: 30px; + font-size: 14px; + transition: 0.2s; + } + + .form-group input:focus, + .form-group textarea:focus, + .form-group select:focus { + border-color: #3b82f6; + outline: none; + } + + .form-group textarea { + min-height: 100px; + resize: vertical; + } + + .form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + } + + .checkbox-group { + display: flex; + align-items: center; + gap: 10px; + } + + .checkbox-group input { + width: auto; + margin-right: 5px; + } + + .tags-input { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 8px; + border: 2px solid #dee9f5; + border-radius: 30px; + background: white; + min-height: 50px; + } + + .tag-input-item { + background: #eef4fa; + padding: 4px 12px; + border-radius: 30px; + font-size: 13px; + color: #1f3f60; + display: inline-flex; + align-items: center; + gap: 5px; + } + + .tag-input-item .remove { + cursor: pointer; + color: #ef4444; + font-size: 12px; + } + + .tag-input-field { + border: none; + outline: none; + padding: 4px 8px; + flex: 1; + min-width: 100px; + } + + .btn-save { + background: #10b981; + color: white; + padding: 12px 24px; + border-radius: 40px; + border: none; + font-weight: 600; + cursor: pointer; + margin-right: 10px; + } + + .btn-save:hover { + background: #059669; + } + + .btn-cancel { + background: #eef4fa; + color: #1f3f60; + padding: 12px 24px; + border-radius: 40px; + border: none; + font-weight: 600; + cursor: pointer; + } + + .btn-cancel:hover { + background: #dbeafe; + } + + .modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 30px; + } + + .resume-experience-item, + .resume-education-item { + background: #f9fcff; + border-radius: 20px; + padding: 15px; + margin-bottom: 15px; + position: relative; + } + + .remove-item-btn { + position: absolute; + top: 10px; + right: 10px; + background: #fee2e2; + border: none; + width: 30px; + height: 30px; + border-radius: 15px; + cursor: pointer; + color: #b91c1c; + } + + .add-item-btn { + background: #eef4fa; + border: none; + padding: 10px 20px; + border-radius: 30px; + cursor: pointer; + margin-top: 10px; + display: inline-flex; + align-items: center; + gap: 8px; + } + + .add-item-btn:hover { + background: #dbeafe; + }
@@ -660,6 +826,116 @@ + +${escapeHtml(exp.description.substring(0, 100))}...
` : ''} +${escapeHtml(exp.description.substring(0, 150))}${exp.description.length > 150 ? '...' : ''}
` : ''}Опыт работы не указан
'; + : 'Опыт работы не указан
'; + // Формируем HTML для образования const educationHtml = resume.education && resume.education.length > 0 ? resume.education.map(edu => ` -Образование не указано
'; + : 'Образование не указано
'; - document.getElementById('resumeModalContent').innerHTML = ` -${escapeHtml(resume.about_me || 'Информация не заполнена')}
+${escapeHtml(resume.about_me || 'Информация не заполнена')}
${escapeHtml(exp.description).replace(/\n/g, '
')}
${escapeHtml(exp.description).replace(/\n/g, '
')}
Опыт работы не указан
'; @@ -1059,7 +1096,9 @@ container.innerHTML = `Контактные данные доступны только авторизованным работодателям
- ${!token ? 'Войти как работодатель' : 'Только для работодателей
'} ++ Контактные данные доступны только авторизованным работодателям +
+ ${!token ? + 'Войти как работодатель' : + 'Только для работодателей
' + }