diff --git a/certbot/.well-known/acme-challenge/index.html b/certbot/.well-known/acme-challenge/index.html new file mode 100755 index 0000000..06efa73 --- /dev/null +++ b/certbot/.well-known/acme-challenge/index.html @@ -0,0 +1,10 @@ + + + + + Title + + +

errr

+ + diff --git a/server.py b/server.py index bafc342..74baacd 100644 --- a/server.py +++ b/server.py @@ -169,6 +169,78 @@ def init_db(): ) """) + # Таблица компаний (для работодателей) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS companies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER UNIQUE NOT NULL, + name TEXT NOT NULL, + description TEXT, + website TEXT, + logo TEXT, + address TEXT, + phone TEXT, + email TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ) + """) + + # Таблица избранного (favorites) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS favorites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + item_type TEXT NOT NULL CHECK(item_type IN ('vacancy', 'resume')), + item_id INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, item_type, item_id), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ) + """) + + # Создаем индексы для быстрого поиска + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_favorites_user + ON favorites(user_id) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_favorites_item + ON favorites(item_type, item_id) + """) + + # Таблица откликов (обновленная версия) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS applications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vacancy_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + message TEXT, + status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'viewed', 'accepted', 'rejected')), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + viewed_at TIMESTAMP, + response_message TEXT, + response_at TIMESTAMP, + FOREIGN KEY (vacancy_id) REFERENCES vacancies (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ) + """) + + # Создаем индексы для быстрого поиска + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_applications_vacancy + ON applications(vacancy_id) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_applications_user + ON applications(user_id) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_applications_status + ON applications(status) + """) + # Создаем админа по умолчанию admin_password = hash_password("admin123") cursor.execute(""" @@ -207,6 +279,30 @@ def init_db(): print("✅ База данных инициализирована") +def set_initial_sequence_ids(): + """Установка начальных ID для таблиц при первом запуске""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, есть ли уже записи в таблицах + cursor.execute("SELECT COUNT(*) FROM vacancies") + vacancies_count = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM resumes") + resumes_count = cursor.fetchone()[0] + + # Если таблицы пустые, устанавливаем начальные ID + if vacancies_count == 0: + cursor.execute("INSERT OR REPLACE INTO sqlite_sequence (name, seq) VALUES ('vacancies', 9999)") + print("✅ Установлен начальный ID для вакансий: 10000") + + if resumes_count == 0: + cursor.execute("INSERT OR REPLACE INTO sqlite_sequence (name, seq) VALUES ('resumes', 9999)") + print("✅ Установлен начальный ID для резюме: 10000") + + conn.commit() + + @contextmanager def get_db(): """Контекстный менеджер для работы с БД""" @@ -304,6 +400,97 @@ class ResumeResponse(BaseModel): updated_at: str full_name: Optional[str] = None +class CompanyCreate(BaseModel): + name: str + description: Optional[str] = None + website: Optional[str] = None + logo: Optional[str] = None + address: Optional[str] = None + phone: Optional[str] = None + email: Optional[str] = None + +class CompanyResponse(BaseModel): + id: int + user_id: int + name: str + description: Optional[str] + website: Optional[str] + logo: Optional[str] + address: Optional[str] + phone: Optional[str] + email: Optional[str] + created_at: str + updated_at: str + + +class FavoriteCreate(BaseModel): + item_type: str # 'vacancy' или 'resume' + item_id: int + + +class FavoriteResponse(BaseModel): + id: int + user_id: int + item_type: str + item_id: int + created_at: str + + # Для расширенной информации + item_data: Optional[dict] = None + + +class FavoriteItem(BaseModel): + id: int + type: str + title: str + subtitle: str + salary: Optional[str] = None + company: Optional[str] = None + name: Optional[str] = None + tags: List[str] = [] + created_at: str + url: str + + +class ApplicationCreate(BaseModel): + vacancy_id: int + message: Optional[str] = None + + +class ApplicationResponse(BaseModel): + id: int + vacancy_id: int + user_id: int + message: Optional[str] + status: str + created_at: str + viewed_at: Optional[str] + response_message: Optional[str] + response_at: Optional[str] + + # Данные о вакансии + vacancy_title: Optional[str] = None + company_name: Optional[str] = None + + # Данные о соискателе + user_name: Optional[str] = None + user_email: Optional[str] = None + user_phone: Optional[str] = None + user_telegram: Optional[str] = None + + +class ApplicationStatusUpdate(BaseModel): + status: str # 'viewed', 'accepted', 'rejected' + response_message: Optional[str] = None + + +class ApplicationStats(BaseModel): + total: int + pending: int + viewed: int + accepted: int + rejected: int + # ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========== @@ -428,6 +615,15 @@ async def get_login(): content="

Страница входа

Создайте файл templates/login.html

") +@app.get("/register", response_class=HTMLResponse) +async def get_register_page(): + """Отдельная страница регистрации""" + file_path = TEMPLATES_DIR / "register.html" + if not file_path.exists(): + return HTMLResponse(content="

Страница регистрации

Создайте файл templates/register.html

") + return FileResponse(file_path) + + @app.get("/register", response_class=HTMLResponse) async def get_register(): """Страница регистрации""" @@ -476,6 +672,25 @@ async def get_resume_detail(resume_id: int): content=f"

Резюме #{resume_id}

Создайте файл templates/resume_detail.html

") +@app.get("/favorites", response_class=HTMLResponse) +async def get_favorites_page(): + """Страница избранного""" + file_path = TEMPLATES_DIR / "favorites.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse(content="

Избранное

Создайте файл templates/favorites.html

") + + +@app.get("/applications", response_class=HTMLResponse) +async def get_applications_page(): + """Страница со списком откликов""" + file_path = TEMPLATES_DIR / "applications.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse(content="

Отклики

Создайте файл templates/applications.html

") + +@app.get("/application/{application_id}", response_class=HTMLResponse) +async def get_application_page(application_id: int): + """Детальная страница отклика""" + file_path = TEMPLATES_DIR / "application_detail.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse(content=f"

Отклик #{application_id}

Создайте файл templates/application_detail.html

") + @app.get("/admin", response_class=HTMLResponse) async def get_admin(): """Страница админки""" @@ -615,22 +830,41 @@ async def create_vacancy( @app.get("/api/vacancies", response_model=List[VacancyResponse]) async def get_my_vacancies(user_id: int = Depends(get_current_user)): - """Получение всех вакансий текущего пользователя""" + """Получение всех вакансий текущего пользователя с данными компании""" with get_db() as conn: cursor = conn.cursor() + + # Получаем компанию пользователя + cursor.execute("SELECT name FROM companies WHERE user_id = ?", (user_id,)) + company = cursor.fetchone() + company_name = company["name"] if company else None + cursor.execute(""" - SELECT v.*, u.full_name as company_name - FROM vacancies v - JOIN users u ON v.user_id = u.id - WHERE v.user_id = ? AND v.is_active = 1 - ORDER BY v.created_at DESC + SELECT * FROM vacancies + WHERE user_id = ? AND is_active = 1 + ORDER BY created_at DESC """, (user_id,)) vacancies = cursor.fetchall() result = [] for v in vacancies: vacancy_dict = dict(v) - vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) + # Добавляем название компании (либо из таблицы companies, либо имя пользователя) + if company_name: + vacancy_dict["company_name"] = company_name + else: + cursor.execute("SELECT full_name FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + vacancy_dict["company_name"] = user["full_name"] if user else None + + # Добавляем теги + cursor.execute(""" + SELECT t.* FROM tags t + JOIN vacancy_tags vt ON t.id = vt.tag_id + WHERE vt.vacancy_id = ? + """, (v["id"],)) + vacancy_dict["tags"] = [dict(tag) for tag in cursor.fetchall()] + result.append(vacancy_dict) return result @@ -640,18 +874,21 @@ async def get_my_vacancies(user_id: int = Depends(get_current_user)): async def get_all_vacancies( page: int = 1, search: str = None, - tags: str = None, # теги через запятую + tags: str = None, min_salary: int = None, sort: str = "newest" ): - """Получение всех вакансий с фильтрацией по тегам""" + """Получение всех вакансий с фильтрацией по тегам и данными компаний""" with get_db() as conn: cursor = conn.cursor() query = """ - SELECT DISTINCT v.*, u.full_name as company_name + SELECT DISTINCT + 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.is_active = 1 """ params = [] @@ -698,7 +935,15 @@ async def get_all_vacancies( result = [] for v in vacancies: vacancy_dict = dict(v) - vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) + + # Добавляем теги для каждой вакансии + cursor.execute(""" + SELECT t.* FROM tags t + JOIN vacancy_tags vt ON t.id = vt.tag_id + WHERE vt.vacancy_id = ? + """, (v["id"],)) + vacancy_dict["tags"] = [dict(tag) for tag in cursor.fetchall()] + result.append(vacancy_dict) # Получаем общее количество @@ -714,7 +959,7 @@ async def get_all_vacancies( @app.get("/api/vacancies/{vacancy_id}") async def get_vacancy(vacancy_id: int, token: Optional[str] = None): - """Получение конкретной вакансии""" + """Получение конкретной вакансии с данными компании""" user_id = get_current_user_optional(token) with get_db() as conn: @@ -723,10 +968,22 @@ async def get_vacancy(vacancy_id: int, token: Optional[str] = None): # Увеличиваем счетчик просмотров cursor.execute("UPDATE vacancies SET views = views + 1 WHERE id = ?", (vacancy_id,)) + # Получаем вакансию с данными компании cursor.execute(""" - SELECT v.*, u.full_name as company_name, u.email, u.telegram + SELECT + v.*, + COALESCE(c.name, u.full_name) as company_name, + c.description as company_description, + c.website as company_website, + c.logo as company_logo, + c.address as company_address, + c.phone as company_phone, + c.email as company_email, + u.email as user_email, + u.telegram as user_telegram 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,)) @@ -735,7 +992,14 @@ async def get_vacancy(vacancy_id: int, token: Optional[str] = None): raise HTTPException(status_code=404, detail="Вакансия не найдена") result = dict(vacancy) - result["tags"] = get_tags_for_vacancy(cursor, vacancy_id) + + # Добавляем теги + cursor.execute(""" + SELECT t.* FROM tags t + JOIN vacancy_tags vt ON t.id = vt.tag_id + WHERE vt.vacancy_id = ? + """, (vacancy_id,)) + result["tags"] = [dict(tag) for tag in cursor.fetchall()] # Проверяем, откликался ли пользователь if user_id: @@ -981,6 +1245,656 @@ async def get_resume(resume_id: int): return resume_data +@app.get("/api/company", response_model=CompanyResponse) +async def get_my_company(user_id: int = Depends(get_current_user)): + """Получение информации о компании текущего пользователя""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, что пользователь - работодатель + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or user["role"] != "employer": + raise HTTPException(status_code=403, detail="Только работодатели могут иметь компанию") + + cursor.execute("SELECT * FROM companies WHERE user_id = ?", (user_id,)) + company = cursor.fetchone() + + if not company: + # Если компании нет, возвращаем пустой объект с 404 + raise HTTPException(status_code=404, detail="Компания не найдена") + + return dict(company) + + +@app.post("/api/company", response_model=CompanyResponse) +async def create_or_update_company( + company: CompanyCreate, + user_id: int = Depends(get_current_user) +): + """Создание или обновление информации о компании""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, что пользователь - работодатель + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or user["role"] != "employer": + raise HTTPException(status_code=403, detail="Только работодатели могут создавать компанию") + + # Проверяем существование компании + cursor.execute("SELECT id FROM companies WHERE user_id = ?", (user_id,)) + existing = cursor.fetchone() + + if existing: + # Обновление существующей компании + cursor.execute(""" + UPDATE companies + SET name = ?, description = ?, website = ?, logo = ?, + address = ?, phone = ?, email = ?, updated_at = CURRENT_TIMESTAMP + WHERE user_id = ? + """, (company.name, company.description, company.website, company.logo, + company.address, company.phone, company.email, user_id)) + + company_id = existing["id"] + else: + # Создание новой компании + cursor.execute(""" + INSERT INTO companies (user_id, name, description, website, logo, address, phone, email) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, (user_id, company.name, company.description, company.website, company.logo, + company.address, company.phone, company.email)) + + company_id = cursor.lastrowid + + conn.commit() + + cursor.execute("SELECT * FROM companies WHERE id = ?", (company_id,)) + return dict(cursor.fetchone()) + + +@app.get("/api/companies/{company_id}") +async def get_company_by_id(company_id: int): + """Получение информации о компании по ID (публичный эндпоинт)""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT c.*, u.full_name as owner_name + FROM companies c + JOIN users u ON c.user_id = u.id + WHERE c.id = ? + """, (company_id,)) + + company = cursor.fetchone() + if not company: + raise HTTPException(status_code=404, detail="Компания не найдена") + + return dict(company) + + +@app.get("/api/resumes/{resume_id}") +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(""" + 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,)) + + resume = cursor.fetchone() + if not resume: + raise HTTPException(status_code=404, detail="Резюме не найдено") + + resume_data = dict(resume) + + # Получаем опыт работы + cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (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 = ?", + (resume_id,)) + resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] + + # Получаем теги + 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()] + + conn.commit() + return resume_data + + +# ========== ЭНДПОИНТЫ ДЛЯ ИЗБРАННОГО ========== + +@app.post("/api/favorites") +async def add_to_favorites( + favorite: FavoriteCreate, + user_id: int = Depends(get_current_user) +): + """Добавление в избранное""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем существование элемента + if favorite.item_type == 'vacancy': + cursor.execute("SELECT id FROM vacancies WHERE id = ? AND is_active = 1", (favorite.item_id,)) + else: # resume + cursor.execute("SELECT id FROM resumes WHERE id = ?", (favorite.item_id,)) + + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail=f"{favorite.item_type} не найден") + + # Добавляем в избранное + try: + cursor.execute(""" + INSERT INTO favorites (user_id, item_type, item_id) + VALUES (?, ?, ?) + """, (user_id, favorite.item_type, favorite.item_id)) + conn.commit() + return {"message": "Добавлено в избранное"} + except sqlite3.IntegrityError: + raise HTTPException(status_code=400, detail="Уже в избранном") + + +@app.delete("/api/favorites") +async def remove_from_favorites( + favorite: FavoriteCreate, + user_id: int = Depends(get_current_user) +): + """Удаление из избранного""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute(""" + DELETE FROM favorites + WHERE user_id = ? AND item_type = ? AND item_id = ? + """, (user_id, favorite.item_type, favorite.item_id)) + + if cursor.rowcount == 0: + raise HTTPException(status_code=404, detail="Не найдено в избранном") + + conn.commit() + return {"message": "Удалено из избранного"} + + +@app.get("/api/favorites") +async def get_favorites( + user_id: int = Depends(get_current_user), + item_type: Optional[str] = None +): + """Получение списка избранного""" + with get_db() as conn: + cursor = conn.cursor() + + query = "SELECT * FROM favorites WHERE user_id = ?" + params = [user_id] + + if item_type: + query += " AND item_type = ?" + params.append(item_type) + + query += " ORDER BY created_at DESC" + + cursor.execute(query, params) + favorites = cursor.fetchall() + + result = [] + for fav in favorites: + fav_dict = dict(fav) + + # Получаем дополнительные данные о элементе + if fav["item_type"] == 'vacancy': + 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 = ? + """, (fav["item_id"],)) + item = cursor.fetchone() + + if item: + item_dict = dict(item) + # Получаем теги вакансии + cursor.execute(""" + SELECT t.name FROM tags t + JOIN vacancy_tags vt ON t.id = vt.tag_id + WHERE vt.vacancy_id = ? + """, (fav["item_id"],)) + tags = [t["name"] for t in cursor.fetchall()] + + fav_dict["item_data"] = { + "id": item["id"], + "title": item["title"], + "company": item_dict.get("company_name"), + "salary": item["salary"], + "tags": tags, + "url": f"/vacancy/{item['id']}" + } + + else: # resume + cursor.execute(""" + SELECT r.*, u.full_name + FROM resumes r + JOIN users u ON r.user_id = u.id + WHERE r.id = ? + """, (fav["item_id"],)) + item = cursor.fetchone() + + if item: + item_dict = dict(item) + # Получаем теги резюме + cursor.execute(""" + SELECT t.name FROM tags t + JOIN resume_tags rt ON t.id = rt.tag_id + WHERE rt.resume_id = ? + """, (fav["item_id"],)) + tags = [t["name"] for t in cursor.fetchall()] + + fav_dict["item_data"] = { + "id": item["id"], + "title": item_dict.get("desired_position", "Резюме"), + "name": item["full_name"], + "salary": item["desired_salary"], + "tags": tags, + "url": f"/resume/{item['id']}" + } + + result.append(fav_dict) + + return result + + +@app.get("/api/favorites/check/{item_type}/{item_id}") +async def check_favorite( + item_type: str, + item_id: int, + user_id: int = Depends(get_current_user) +): + """Проверка, находится ли элемент в избранном""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT id FROM favorites + WHERE user_id = ? AND item_type = ? AND item_id = ? + """, (user_id, item_type, item_id)) + + return {"is_favorite": cursor.fetchone() is not None} + + +@app.get("/api/user/stats") +async def get_user_stats(user_id: int = Depends(get_current_user)): + """Получение статистики пользователя (количество избранного, просмотров и т.д.)""" + with get_db() as conn: + cursor = conn.cursor() + + stats = {} + + # Количество избранных вакансий + cursor.execute(""" + SELECT COUNT(*) FROM favorites + WHERE user_id = ? AND item_type = 'vacancy' + """, (user_id,)) + stats["favorite_vacancies"] = cursor.fetchone()[0] + + # Количество избранных резюме + cursor.execute(""" + SELECT COUNT(*) FROM favorites + WHERE user_id = ? AND item_type = 'resume' + """, (user_id,)) + stats["favorite_resumes"] = cursor.fetchone()[0] + + # Общее количество избранного + stats["total_favorites"] = stats["favorite_vacancies"] + stats["favorite_resumes"] + + # Количество просмотров профиля (если есть резюме) + cursor.execute(""" + SELECT SUM(views) FROM resumes WHERE user_id = ? + """, (user_id,)) + stats["profile_views"] = cursor.fetchone()[0] or 0 + + # Количество созданных вакансий (для работодателя) + cursor.execute(""" + SELECT COUNT(*) FROM vacancies + WHERE user_id = ? AND is_active = 1 + """, (user_id,)) + stats["active_vacancies"] = cursor.fetchone()[0] + + return stats + + +# ========== ЭНДПОИНТЫ ДЛЯ ОТКЛИКОВ ========== + +@app.post("/api/applications") +async def create_application( + application: ApplicationCreate, + user_id: int = Depends(get_current_user) +): + """Создание отклика на вакансию""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, что пользователь - соискатель + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or user["role"] != "employee": + raise HTTPException(status_code=403, detail="Только соискатели могут откликаться на вакансии") + + # Проверяем существование вакансии + cursor.execute(""" + SELECT v.*, u.id as employer_id + FROM vacancies v + JOIN users u ON v.user_id = u.id + WHERE v.id = ? AND v.is_active = 1 + """, (application.vacancy_id,)) + + vacancy = cursor.fetchone() + if not vacancy: + raise HTTPException(status_code=404, detail="Вакансия не найдена") + + # Проверяем, не откликался ли уже + cursor.execute(""" + SELECT id FROM applications + WHERE vacancy_id = ? AND user_id = ? + """, (application.vacancy_id, user_id)) + + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Вы уже откликались на эту вакансию") + + # Создаем отклик + cursor.execute(""" + INSERT INTO applications (vacancy_id, user_id, message) + VALUES (?, ?, ?) + """, (application.vacancy_id, user_id, application.message)) + + application_id = cursor.lastrowid + conn.commit() + + # Получаем созданный отклик + cursor.execute("SELECT * FROM applications WHERE id = ?", (application_id,)) + new_application = dict(cursor.fetchone()) + + return new_application + + +@app.get("/api/applications/received") +async def get_received_applications( + user_id: int = Depends(get_current_user), + status: Optional[str] = None, + page: int = 1, + limit: int = 20 +): + """Получение откликов на вакансии работодателя (входящие)""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, что пользователь - работодатель + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or user["role"] != "employer": + raise HTTPException(status_code=403, detail="Только работодатели могут просматривать отклики") + + query = """ + SELECT + a.*, + v.title as vacancy_title, + v.salary as vacancy_salary, + u.full_name as user_name, + u.email as user_email, + u.phone as user_phone, + u.telegram as user_telegram + FROM applications a + JOIN vacancies v ON a.vacancy_id = v.id + JOIN users u ON a.user_id = u.id + WHERE v.user_id = ? + """ + params = [user_id] + + if status: + query += " AND a.status = ?" + params.append(status) + + # Пагинация + offset = (page - 1) * limit + query += " ORDER BY a.created_at DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + cursor.execute(query, params) + applications = cursor.fetchall() + + # Получаем общее количество + count_query = "SELECT COUNT(*) FROM applications a JOIN vacancies v ON a.vacancy_id = v.id WHERE v.user_id = ?" + count_params = [user_id] + + if status: + count_query += " AND a.status = ?" + count_params.append(status) + + cursor.execute(count_query, count_params) + total = cursor.fetchone()[0] + + result = [] + for app in applications: + app_dict = dict(app) + result.append(app_dict) + + return { + "applications": result, + "total": total, + "page": page, + "total_pages": (total + limit - 1) // limit + } + + +@app.get("/api/applications/sent") +async def get_sent_applications( + user_id: int = Depends(get_current_user), + status: Optional[str] = None, + page: int = 1, + limit: int = 20 +): + """Получение отправленных откликов (для соискателя)""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, что пользователь - соискатель + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or user["role"] != "employee": + raise HTTPException(status_code=403, detail="Только соискатели могут просматривать свои отклики") + + query = """ + SELECT + a.*, + v.title as vacancy_title, + v.salary as vacancy_salary, + COALESCE(c.name, u_emp.full_name) as company_name, + u_emp.email as employer_email, + u_emp.telegram as employer_telegram + FROM applications a + JOIN vacancies v ON a.vacancy_id = v.id + JOIN users u_emp ON v.user_id = u_emp.id + LEFT JOIN companies c ON u_emp.id = c.user_id + WHERE a.user_id = ? + """ + params = [user_id] + + if status: + query += " AND a.status = ?" + params.append(status) + + # Пагинация + offset = (page - 1) * limit + query += " ORDER BY a.created_at DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + cursor.execute(query, params) + applications = cursor.fetchall() + + # Получаем общее количество + cursor.execute("SELECT COUNT(*) FROM applications WHERE user_id = ?", (user_id,)) + total = cursor.fetchone()[0] + + result = [] + for app in applications: + app_dict = dict(app) + result.append(app_dict) + + return { + "applications": result, + "total": total, + "page": page, + "total_pages": (total + limit - 1) // limit + } + + +@app.get("/api/applications/{application_id}") +async def get_application( + application_id: int, + user_id: int = Depends(get_current_user) +): + """Получение детальной информации об отклике""" + with get_db() as conn: + cursor = conn.cursor() + + # Получаем отклик с данными + cursor.execute(""" + SELECT + a.*, + v.title as vacancy_title, + v.description as vacancy_description, + v.salary as vacancy_salary, + v.contact as vacancy_contact, + u_applicant.full_name as applicant_name, + u_applicant.email as applicant_email, + u_applicant.phone as applicant_phone, + u_applicant.telegram as applicant_telegram, + u_employer.full_name as employer_name, + u_employer.email as employer_email, + u_employer.telegram as employer_telegram, + c.name as company_name, + c.description as company_description + FROM applications a + JOIN vacancies v ON a.vacancy_id = v.id + JOIN users u_applicant ON a.user_id = u_applicant.id + JOIN users u_employer ON v.user_id = u_employer.id + LEFT JOIN companies c ON u_employer.id = c.user_id + WHERE a.id = ? + """, (application_id,)) + + application = cursor.fetchone() + if not application: + raise HTTPException(status_code=404, detail="Отклик не найден") + + # Проверяем права доступа (либо автор отклика, либо владелец вакансии) + if application["user_id"] != user_id and application["v_user_id"] != user_id: + raise HTTPException(status_code=403, detail="Нет доступа к этому отклику") + + # Если просматривает работодатель и статус 'pending', меняем на 'viewed' + if application["v_user_id"] == user_id and application["status"] == 'pending': + cursor.execute(""" + UPDATE applications + SET status = 'viewed', viewed_at = CURRENT_TIMESTAMP + WHERE id = ? + """, (application_id,)) + conn.commit() + application = dict(application) + application["status"] = 'viewed' + + return dict(application) + + +@app.put("/api/applications/{application_id}/status") +async def update_application_status( + application_id: int, + status_update: ApplicationStatusUpdate, + user_id: int = Depends(get_current_user) +): + """Обновление статуса отклика (для работодателя)""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, что отклик существует и пользователь - владелец вакансии + cursor.execute(""" + SELECT a.*, v.user_id as employer_id + FROM applications a + JOIN vacancies v ON a.vacancy_id = v.id + WHERE a.id = ? + """, (application_id,)) + + application = cursor.fetchone() + if not application: + raise HTTPException(status_code=404, detail="Отклик не найден") + + if application["employer_id"] != user_id: + raise HTTPException(status_code=403, detail="Только владелец вакансии может изменять статус") + + # Обновляем статус + cursor.execute(""" + UPDATE applications + SET status = ?, response_message = ?, response_at = CURRENT_TIMESTAMP + WHERE id = ? + """, (status_update.status, status_update.response_message, application_id)) + + conn.commit() + + return {"message": "Статус обновлен"} + + +@app.get("/api/applications/stats") +async def get_application_stats(user_id: int = Depends(get_current_user)): + """Получение статистики по откликам""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем роль пользователя + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + + stats = {} + + if user["role"] == "employer": + # Статистика для работодателя (полученные отклики) + cursor.execute(""" + SELECT + COUNT(*) as total, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN status = 'viewed' THEN 1 ELSE 0 END) as viewed, + SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) as accepted, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected + FROM applications a + JOIN vacancies v ON a.vacancy_id = v.id + WHERE v.user_id = ? + """, (user_id,)) + + else: + # Статистика для соискателя (отправленные отклики) + cursor.execute(""" + SELECT + COUNT(*) as total, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN status = 'viewed' THEN 1 ELSE 0 END) as viewed, + SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) as accepted, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected + FROM applications + WHERE user_id = ? + """, (user_id,)) + + stats = dict(cursor.fetchone()) + return stats + + # ========== АДМИНСКИЕ ЭНДПОИНТЫ ========== @app.get("/api/admin/stats") @@ -1160,6 +2074,7 @@ async def get_user_contact( # Инициализация базы данных init_db() +set_initial_sequence_ids() if __name__ == "__main__": print("🚀 Запуск сервера на http://localhost:8000") diff --git a/templates/application_detail.html b/templates/application_detail.html new file mode 100644 index 0000000..c7aa237 --- /dev/null +++ b/templates/application_detail.html @@ -0,0 +1,358 @@ + + + + + + Отклик | Rabota.Today + + + + +
+
+ + +
+ + Назад к откликам + +
+
Загрузка...
+
+
+ + + + \ No newline at end of file diff --git a/templates/applications.html b/templates/applications.html new file mode 100644 index 0000000..f51edf1 --- /dev/null +++ b/templates/applications.html @@ -0,0 +1,1040 @@ + + + + + + Отклики | Rabota.Today + + + + +
+
+ + +
+ +
+

+ + Отклики +

+
+ + +
+
+ + +
+
+
0
+
Ожидают
+
+
+
0
+
Просмотрены
+
+
+
0
+
Приняты
+
+
+
0
+
Отклонены
+
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ Загрузка откликов... +
+
+ + + +
+ + + + +
+ + + + \ No newline at end of file diff --git a/templates/favorites.html b/templates/favorites.html new file mode 100644 index 0000000..e4bf144 --- /dev/null +++ b/templates/favorites.html @@ -0,0 +1,864 @@ + + + + + + Избранное | Rabota.Today + + + + +
+
+ + +
+ + + Вернуться в профиль + + +
+

+ + Избранное +

+
+
Все
+
Вакансии
+
Резюме
+
+
+ + +
+
+ 0 + всего +
+
+ 0 + вакансий +
+
+ 0 + резюме +
+
+ +
+
+ +
Загрузка избранного...
+
+
+
+ +
+ + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4180cf3..9cc7719 100644 --- a/templates/index.html +++ b/templates/index.html @@ -170,7 +170,7 @@
\ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..9bf8b87 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,699 @@ + + + + + + Регистрация | Rabota.Today + + + + +
+
+

+ + МП.Ярмарка +

+

Создайте аккаунт для доступа к ярмарке вакансий

+
+ +
+ + + + + + +
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + +
+

Требования к паролю:

+
+ Минимум 6 символов +
+
+ Пароли совпадают +
+
+ + +
+ + +
+ + + +
+ + + +
+ + +
+ + + + + + + \ No newline at end of file diff --git a/templates/resume_detail.html b/templates/resume_detail.html new file mode 100644 index 0000000..906b1f6 --- /dev/null +++ b/templates/resume_detail.html @@ -0,0 +1,641 @@ + + + + + + Резюме | Rabota.Today + + + + +
+
+ + +
+ + Назад к резюме + +
+
Загрузка резюме...
+
+
+ + + + \ No newline at end of file diff --git a/templates/resumes.html b/templates/resumes.html index 556d3f7..a6a654f 100644 --- a/templates/resumes.html +++ b/templates/resumes.html @@ -289,7 +289,7 @@