diff --git a/certbot/.well-known/acme-challenge/test b/certbot/.well-known/acme-challenge/test new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/certbot/.well-known/acme-challenge/test @@ -0,0 +1 @@ +test diff --git a/migrate_experience.py b/migrate_experience.py new file mode 100644 index 0000000..24c41ff --- /dev/null +++ b/migrate_experience.py @@ -0,0 +1,49 @@ +# migrate_experience.py +import sqlite3 + +DB_NAME = "rabota_today.db" + + +def add_description_column(): + """Добавление поля description в таблицу work_experience""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + try: + # Проверяем существующие колонки + cursor.execute("PRAGMA table_info(work_experience)") + columns = [col[1] for col in cursor.fetchall()] + + print("📊 Текущие колонки в таблице work_experience:", columns) + + # Добавляем колонку description, если её нет + if 'description' not in columns: + print("➕ Добавление колонки description...") + cursor.execute("ALTER TABLE work_experience ADD COLUMN description TEXT") + print("✅ Колонка description добавлена") + + conn.commit() + print("\n🎉 Миграция успешно завершена!") + + # Показываем обновленную структуру + cursor.execute("PRAGMA table_info(work_experience)") + new_columns = [col[1] for col in cursor.fetchall()] + print("\n📊 Обновленные колонки:", new_columns) + + except Exception as e: + print(f"❌ Ошибка: {e}") + conn.rollback() + finally: + conn.close() + + +if __name__ == "__main__": + print("=" * 50) + print("🚀 Миграция таблицы work_experience") + print("=" * 50) + + response = input("Добавить поле description? (y/n): ") + if response.lower() == 'y': + add_description_column() + else: + print("❌ Миграция отменена") \ No newline at end of file diff --git a/server.py b/server.py index 74baacd..573c7b0 100644 --- a/server.py +++ b/server.py @@ -14,6 +14,12 @@ import os from contextlib import contextmanager import uvicorn from pathlib import Path +import traceback +from fastapi.middleware.trustedhost import TrustedHostMiddleware + +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +import re app = FastAPI(title="Rabota.Today API", version="2.0.0") @@ -131,7 +137,7 @@ def init_db(): ) """) - # Таблица опыта работы + # Таблица опыта работы (ОБНОВЛЕНА - добавлено поле description) cursor.execute(""" CREATE TABLE IF NOT EXISTS work_experience ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -139,6 +145,7 @@ def init_db(): position TEXT NOT NULL, company TEXT NOT NULL, period TEXT, + description TEXT, FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE ) """) @@ -155,61 +162,6 @@ def init_db(): ) """) - # Таблица откликов - 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, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - status TEXT DEFAULT 'pending', - FOREIGN KEY (vacancy_id) REFERENCES vacancies (id), - FOREIGN KEY (user_id) REFERENCES users (id) - ) - """) - - # Таблица компаний (для работодателей) - 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 ( @@ -227,19 +179,43 @@ def init_db(): ) """) + # Таблица избранного + 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 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 + ) + """) + # Создаем индексы для быстрого поиска - 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) - """) + 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)") + 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)") # Создаем админа по умолчанию admin_password = hash_password("admin123") @@ -366,10 +342,20 @@ class VacancyResponse(BaseModel): company_name: Optional[str] = None +class VacancyUpdate(BaseModel): + title: Optional[str] = None + salary: Optional[str] = None + description: Optional[str] = None + contact: Optional[str] = None + tags: Optional[List[str]] = None + is_active: Optional[bool] = None + + class WorkExperience(BaseModel): position: str company: str period: Optional[str] = None + description: Optional[str] = None class Education(BaseModel): @@ -506,24 +492,53 @@ def verify_password(password: str, hash: str) -> bool: def create_access_token(data: dict): - """Создание JWT токена""" + """Создание JWT токена с явным преобразованием ID в строку""" to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) + + # Убеждаемся, что user_id сохраняется как строка + if "sub" in to_encode: + to_encode["sub"] = str(to_encode["sub"]) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): - """Получение текущего пользователя из токена""" + """Получение текущего пользователя из токена с улучшенной обработкой ошибок""" token = credentials.credentials + try: + # Декодируем токен payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + + # Получаем user_id из поля sub user_id = payload.get("sub") + if user_id is None: - raise HTTPException(status_code=401, detail="Невалидный токен") - return int(user_id) - except jwt.PyJWTError: - raise HTTPException(status_code=401, detail="Невалидный токен") + print("❌ No sub in token payload") + raise HTTPException(status_code=401, detail="Невалидный токен: отсутствует идентификатор пользователя") + + # Пробуем преобразовать в整数 + try: + user_id_int = int(user_id) + except (ValueError, TypeError) as e: + print(f"❌ Cannot convert user_id to int: {user_id}, error: {e}") + raise HTTPException(status_code=401, detail=f"Невалидный формат ID пользователя: {user_id}") + + print(f"✅ User authenticated: {user_id_int}") + return user_id_int + + except jwt.ExpiredSignatureError: + print("❌ Token expired") + raise HTTPException(status_code=401, detail="Токен истек") + except jwt.InvalidTokenError as e: + print(f"❌ Invalid token: {e}") + raise HTTPException(status_code=401, detail=f"Невалидный токен: {str(e)}") + except Exception as e: + print(f"❌ Unexpected error in get_current_user: {e}") + traceback.print_exc() + raise HTTPException(status_code=401, detail=f"Ошибка авторизации: {str(e)}") def get_current_user_optional(token: Optional[str] = None): @@ -598,13 +613,28 @@ def add_tags_to_resume(cursor, resume_id, tag_names): # ========== HTML СТРАНИЦЫ ========== @app.get("/", response_class=HTMLResponse) -async def get_index(): - """Главная страница""" - file_path = TEMPLATES_DIR / "index.html" - if not file_path.exists(): - return HTMLResponse( - content="

Rabota.Today API

Сервер работает. Создайте файл templates/index.html

") - return FileResponse(file_path) +async def get_index(request: Request): + """Главная страница с определением устройства""" + + # Определяем мобильное устройство по User-Agent + user_agent = request.headers.get("user-agent", "").lower() + is_mobile = any([ + "mobile" in user_agent, + "android" in user_agent, + "iphone" in user_agent, + "ipad" in user_agent + ]) + + if is_mobile: + file_path = TEMPLATES_DIR / "index_mobile.html" + else: + file_path = TEMPLATES_DIR / "index.html" + + if file_path.exists(): + with open(file_path, "r", encoding="utf-8") as f: + return HTMLResponse(content=f.read()) + + return HTMLResponse(content="

Rabota.Today

Страница не найдена

") @app.get("/login", response_class=HTMLResponse) @@ -615,6 +645,52 @@ async def get_login(): content="

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

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

") +@app.get("/companies", response_class=HTMLResponse) +async def get_companies_page(): + """Страница со списком компаний""" + file_path = TEMPLATES_DIR / "companies.html" + if file_path.exists(): + with open(file_path, "r", encoding="utf-8") as f: + return HTMLResponse(content=f.read()) + return HTMLResponse(content="

Компании

Страница в разработке

") + + +@app.get("/company/{company_id}", response_class=HTMLResponse) +async def get_company_page(company_id: int): + """Страница компании""" + file_path = TEMPLATES_DIR / "company_detail.html" + if file_path.exists(): + with open(file_path, "r", encoding="utf-8") as f: + return HTMLResponse(content=f.read()) + return HTMLResponse(content="

Страница компании

Файл не найден

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

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

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

") + +@app.get("/debug", response_class=HTMLResponse) +async def get_debug_page(): + """Страница диагностики для мобильных устройств""" + file_path = "templates/mobile_debug_simple.html" + if os.path.exists(file_path): + with open(file_path, "r", encoding="utf-8") as f: + return HTMLResponse(content=f.read()) + return HTMLResponse(content=""" + + + Debug + +

Debug page not found

+

Create file: templates/mobile_debug_simple.html

+ + + """) + + @app.get("/register", response_class=HTMLResponse) async def get_register_page(): """Отдельная страница регистрации""" @@ -701,60 +777,159 @@ async def get_admin(): # ========== API ЭНДПОИНТЫ ========== -@app.post("/api/register", response_model=TokenResponse) -async def register(user: UserRegister): - """Регистрация нового пользователя""" - with get_db() as conn: - cursor = conn.cursor() +@app.get("/api/public/stats") +async def get_public_stats(): + """Публичная статистика для главной страницы""" + try: + with get_db() as conn: + cursor = conn.cursor() - cursor.execute("SELECT id FROM users WHERE email = ?", (user.email,)) - if cursor.fetchone(): - raise HTTPException(status_code=400, detail="Email уже зарегистрирован") + # Активные вакансии + cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1") + active_vacancies = cursor.fetchone()[0] - password_hash = hash_password(user.password) - is_admin = 1 if user.email == "admin@rabota.today" else 0 + # Все резюме + cursor.execute("SELECT COUNT(*) FROM resumes") + total_resumes = cursor.fetchone()[0] - cursor.execute(""" - INSERT INTO users (full_name, email, phone, telegram, password_hash, role, is_admin) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, (user.full_name, user.email, user.phone, user.telegram, password_hash, user.role, is_admin)) + # Работодатели (компании) + cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'") + total_employers = cursor.fetchone()[0] - user_id = cursor.lastrowid - conn.commit() - - token = create_access_token({"sub": str(user_id), "role": user.role, "is_admin": is_admin}) + # Все пользователи + cursor.execute("SELECT COUNT(*) FROM users") + total_users = cursor.fetchone()[0] + return { + "active_vacancies": active_vacancies, + "total_resumes": total_resumes, + "total_employers": total_employers, + "total_users": total_users + } + except Exception as e: + print(f"Error getting public stats: {e}") return { - "access_token": token, - "token_type": "bearer", - "user_id": user_id, - "full_name": user.full_name, - "role": user.role, - "is_admin": bool(is_admin) + "active_vacancies": 0, + "total_resumes": 0, + "total_employers": 0, + "total_users": 0 } +@app.post("/api/register", response_model=TokenResponse) +async def register(user: UserRegister, request: Request): + """Регистрация нового пользователя с поддержкой мобильных устройств""" + try: + # Валидация email + if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", user.email): + raise HTTPException(status_code=400, detail="Неверный формат email") + + # Валидация телефона + phone_digits = re.sub(r"\D", "", user.phone) + if len(phone_digits) < 10: + raise HTTPException(status_code=400, detail="Неверный формат телефона") + + with get_db() as conn: + cursor = conn.cursor() + + # Проверка существования пользователя + cursor.execute("SELECT id FROM users WHERE email = ?", (user.email,)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Email уже зарегистрирован") + + # Создание пользователя + password_hash = hash_password(user.password) + is_admin = 1 if user.email == "admin@rabota.today" else 0 + + cursor.execute(""" + INSERT INTO users (full_name, email, phone, telegram, password_hash, role, is_admin) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (user.full_name, user.email, user.phone, user.telegram, password_hash, user.role, is_admin)) + + user_id = cursor.lastrowid + conn.commit() + + # Создание токена с дополнительной информацией + token_data = { + "sub": str(user_id), + "role": user.role, + "is_admin": bool(is_admin), + "device": "mobile" if request.state.is_mobile else "desktop" + } + token = create_access_token(token_data) + + response_data = { + "access_token": token, + "token_type": "bearer", + "user_id": user_id, + "full_name": user.full_name, + "role": user.role, + "is_admin": bool(is_admin) + } + + # Добавляем заголовки для мобильных устройств + if request.state.is_mobile: + return JSONResponse( + content=response_data, + headers={ + "Access-Control-Allow-Credentials": "true", + "Access-Control-Expose-Headers": "Authorization" + } + ) + + return response_data + + except HTTPException: + raise + except Exception as e: + print(f"Registration error: {e}") + raise HTTPException(status_code=500, detail="Внутренняя ошибка сервера") + @app.post("/api/login", response_model=TokenResponse) -async def login(user_data: UserLogin): - """Вход в систему""" - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM users WHERE email = ?", (user_data.email,)) - user = cursor.fetchone() +async def login(user_data: UserLogin, request: Request): + """Вход в систему с поддержкой мобильных устройств""" + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE email = ?", (user_data.email,)) + user = cursor.fetchone() - if not user or not verify_password(user_data.password, user["password_hash"]): - raise HTTPException(status_code=401, detail="Неверный email или пароль") + if not user or not verify_password(user_data.password, user["password_hash"]): + raise HTTPException(status_code=401, detail="Неверный email или пароль") - token = create_access_token({"sub": str(user["id"]), "role": user["role"], "is_admin": user["is_admin"]}) + token_data = { + "sub": str(user["id"]), + "role": user["role"], + "is_admin": user["is_admin"], + "device": "mobile" if request.state.is_mobile else "desktop" + } + token = create_access_token(token_data) - return { - "access_token": token, - "token_type": "bearer", - "user_id": user["id"], - "full_name": user["full_name"], - "role": user["role"], - "is_admin": bool(user["is_admin"]) - } + response_data = { + "access_token": token, + "token_type": "bearer", + "user_id": user["id"], + "full_name": user["full_name"], + "role": user["role"], + "is_admin": bool(user["is_admin"]) + } + + if request.state.is_mobile: + return JSONResponse( + content=response_data, + headers={ + "Access-Control-Allow-Credentials": "true", + "Access-Control-Expose-Headers": "Authorization" + } + ) + + return response_data + + except HTTPException: + raise + except Exception as e: + print(f"Login error: {e}") + raise HTTPException(status_code=500, detail="Внутренняя ошибка сервера") @app.get("/api/tags") @@ -876,9 +1051,10 @@ async def get_all_vacancies( search: str = None, tags: str = None, min_salary: int = None, - sort: str = "newest" + sort: str = "newest", + company_id: int = None # Новый параметр ): - """Получение всех вакансий с фильтрацией по тегам и данными компаний""" + """Получение всех вакансий с фильтрацией""" with get_db() as conn: cursor = conn.cursor() @@ -893,6 +1069,12 @@ async def get_all_vacancies( """ params = [] + # Фильтр по компании + if company_id: + query += " AND c.id = ?" + params.append(company_id) + + # Остальные фильтры... if search: query += " AND (v.title LIKE ? OR v.description LIKE ?)" params.extend([f"%{search}%", f"%{search}%"]) @@ -936,7 +1118,7 @@ async def get_all_vacancies( for v in vacancies: vacancy_dict = dict(v) - # Добавляем теги для каждой вакансии + # Добавляем теги cursor.execute(""" SELECT t.* FROM tags t JOIN vacancy_tags vt ON t.id = vt.tag_id @@ -947,7 +1129,12 @@ async def get_all_vacancies( result.append(vacancy_dict) # Получаем общее количество - cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1") + count_query = "SELECT COUNT(*) FROM vacancies WHERE is_active = 1" + if company_id: + count_query += " AND user_id = (SELECT user_id FROM companies WHERE id = ?)" + cursor.execute(count_query, (company_id,)) + else: + cursor.execute(count_query) total = cursor.fetchone()[0] return { @@ -957,6 +1144,53 @@ async def get_all_vacancies( } +@app.get("/api/vacancies/my-all") +async def get_my_all_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 + + # Получаем ВСЕ вакансии пользователя без фильтра по is_active + cursor.execute(""" + SELECT * FROM vacancies + WHERE user_id = ? + ORDER BY + is_active DESC, -- сначала активные + created_at DESC -- потом по дате + """, (user_id,)) + + vacancies = cursor.fetchall() + + result = [] + for v in vacancies: + vacancy_dict = dict(v) + 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 + + @app.get("/api/vacancies/{vacancy_id}") async def get_vacancy(vacancy_id: int, token: Optional[str] = None): """Получение конкретной вакансии с данными компании""" @@ -1013,6 +1247,107 @@ async def get_vacancy(vacancy_id: int, token: Optional[str] = None): return result +@app.put("/api/vacancies/{vacancy_id}") +async def update_vacancy( + vacancy_id: int, + vacancy_update: VacancyUpdate, + user_id: int = Depends(get_current_user) +): + """Обновление существующей вакансии""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, что вакансия принадлежит пользователю + cursor.execute(""" + SELECT v.*, u.role + FROM vacancies v + JOIN users u ON v.user_id = u.id + WHERE v.id = ? AND v.user_id = ? + """, (vacancy_id, user_id)) + + vacancy = cursor.fetchone() + if not vacancy: + raise HTTPException(status_code=404, detail="Вакансия не найдена") + + # Проверяем роль (только работодатель может редактировать) + if vacancy["role"] != "employer": + raise HTTPException(status_code=403, detail="Только работодатели могут редактировать вакансии") + + # Обновляем основные поля + update_fields = [] + params = [] + + if vacancy_update.title is not None: + update_fields.append("title = ?") + params.append(vacancy_update.title) + + if vacancy_update.salary is not None: + update_fields.append("salary = ?") + params.append(vacancy_update.salary) + + if vacancy_update.description is not None: + update_fields.append("description = ?") + params.append(vacancy_update.description) + + if vacancy_update.contact is not None: + update_fields.append("contact = ?") + params.append(vacancy_update.contact) + + if vacancy_update.is_active is not None: + update_fields.append("is_active = ?") + params.append(1 if vacancy_update.is_active else 0) + + if update_fields: + query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?" + params.append(vacancy_id) + cursor.execute(query, params) + + # Обновляем теги + if vacancy_update.tags is not None: + # Удаляем старые теги + cursor.execute("DELETE FROM vacancy_tags WHERE vacancy_id = ?", (vacancy_id,)) + + # Добавляем новые теги + for tag_name in vacancy_update.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(""" + INSERT INTO vacancy_tags (vacancy_id, tag_id) + VALUES (?, ?) + """, (vacancy_id, tag_id)) + + conn.commit() + + # Возвращаем обновленную вакансию + 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,)) + + updated_vacancy = dict(cursor.fetchone()) + + # Добавляем теги + 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()] + + return updated_vacancy + + @app.post("/api/vacancies/{vacancy_id}/apply") async def apply_to_vacancy( vacancy_id: int, @@ -1054,15 +1389,18 @@ async def create_or_update_resume( 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 id FROM resumes WHERE user_id = ?", (user_id,)) existing = cursor.fetchone() if existing: + # Обновление существующего резюме cursor.execute(""" UPDATE resumes SET desired_position = ?, about_me = ?, desired_salary = ?, updated_at = CURRENT_TIMESTAMP @@ -1071,10 +1409,12 @@ async def create_or_update_resume( resume_id = existing["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,)) else: + # Создание нового резюме cursor.execute(""" INSERT INTO resumes (user_id, desired_position, about_me, desired_salary) VALUES (?, ?, ?, ?) @@ -1082,35 +1422,63 @@ async def create_or_update_resume( resume_id = cursor.lastrowid + # Добавление опыта работы (ОБНОВЛЕНО - добавили description) for exp in resume.work_experience: cursor.execute(""" - INSERT INTO work_experience (resume_id, position, company, period) - VALUES (?, ?, ?, ?) - """, (resume_id, exp.position, exp.company, exp.period)) + 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: - add_tags_to_resume(cursor, resume_id, 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(""" + 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 position, company, period FROM work_experience 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()] - cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?", - (resume_id,)) + 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_data["tags"] = get_tags_for_resume(cursor, resume_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()] return resume_data @@ -1129,14 +1497,36 @@ async def get_my_resume(user_id: int = Depends(get_current_user)): resume_data = dict(resume) - cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume["id"],)) + # Получаем опыт работы (ОБНОВЛЕНО - добавили поле 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 + """, (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"],)) + # Получаем образование + 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()] - resume_data["tags"] = get_tags_for_resume(cursor, resume["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()] return resume_data @@ -1216,6 +1606,9 @@ 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 @@ -1227,19 +1620,38 @@ async def get_resume(resume_id: int): if not resume: raise HTTPException(status_code=404, detail="Резюме не найдено") - # Увеличиваем счетчик просмотров - cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,)) - resume_data = dict(resume) - cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume_id,)) + # Получаем опыт работы (ОБНОВЛЕНО - добавили поле 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 + """, (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,)) + # Получаем образование + 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()] - resume_data["tags"] = get_tags_for_resume(cursor, resume_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()] conn.commit() return resume_data @@ -1636,65 +2048,153 @@ async def get_received_applications( limit: int = 20 ): """Получение откликов на вакансии работодателя (входящие)""" - with get_db() as conn: - cursor = conn.cursor() + try: + 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 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] + # Исправлено: убрали лишние поля, добавили правильные алиасы + 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) + 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]) + # Пагинация + 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(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] + # Получаем общее количество + 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) + if status: + count_query += " AND a.status = ?" + count_params.append(status) - cursor.execute(count_query, count_params) - total = cursor.fetchone()[0] + cursor.execute(count_query, count_params) + total = cursor.fetchone()[0] - result = [] - for app in applications: - app_dict = dict(app) - result.append(app_dict) + 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 - } + return { + "applications": result, + "total": total, + "page": page, + "total_pages": (total + limit - 1) // limit + } + + except HTTPException: + raise + except Exception as e: + print(f"Error in get_received_applications: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/applications/stats") +async def get_application_stats(user_id: int = Depends(get_current_user)): + """Получение статистики по откликам""" + try: + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем существование пользователя + cursor.execute("SELECT id, role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Базовая структура статистики + stats = { + "total": 0, + "pending": 0, + "viewed": 0, + "accepted": 0, + "rejected": 0 + } + + if user["role"] == "employer": + # Статистика для работодателя (полученные отклики) + cursor.execute(""" + SELECT + COUNT(*) as total, + SUM(CASE WHEN a.status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN a.status = 'viewed' THEN 1 ELSE 0 END) as viewed, + SUM(CASE WHEN a.status = 'accepted' THEN 1 ELSE 0 END) as accepted, + SUM(CASE WHEN a.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,)) + + row = cursor.fetchone() + if row: + stats = { + "total": row[0] or 0, + "pending": row[1] or 0, + "viewed": row[2] or 0, + "accepted": row[3] or 0, + "rejected": row[4] or 0 + } + + elif user["role"] == "employee": + # Статистика для соискателя (отправленные отклики) + 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,)) + + row = cursor.fetchone() + if row: + stats = { + "total": row[0] or 0, + "pending": row[1] or 0, + "viewed": row[2] or 0, + "accepted": row[3] or 0, + "rejected": row[4] or 0 + } + + # Добавляем информацию о роли для отладки + stats["role"] = user["role"] + + return stats + + except Exception as e: + print(f"❌ Error in get_application_stats: {e}") + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") @app.get("/api/applications/sent") @@ -1705,114 +2205,65 @@ async def get_sent_applications( limit: int = 20 ): """Получение отправленных откликов (для соискателя)""" - with get_db() as conn: - cursor = conn.cursor() + try: + with get_db() as conn: + cursor = conn.cursor() - # Проверяем, что пользователь - соискатель - cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) - user = cursor.fetchone() - if not user or user["role"] != "employee": - raise HTTPException(status_code=403, detail="Только соискатели могут просматривать свои отклики") + # Проверяем, что пользователь - соискатель + cursor.execute("SELECT role 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] + 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) + 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]) + # Пагинация + 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(query, params) + applications = cursor.fetchall() - # Получаем общее количество - cursor.execute("SELECT COUNT(*) FROM applications WHERE user_id = ?", (user_id,)) - total = cursor.fetchone()[0] + # Получаем общее количество + 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) + 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 - } + 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) + except HTTPException: + raise + except Exception as e: + print(f"Error in get_sent_applications: {e}") + raise HTTPException(status_code=500, detail=str(e)) @app.put("/api/applications/{application_id}/status") @@ -1852,47 +2303,83 @@ async def update_application_status( 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() +@app.get("/api/applications/{application_id}") +async def get_application( + application_id: int, + user_id: int = Depends(get_current_user) +): + """Получение детальной информации об отклике""" + try: + print(f"📄 Getting application details for ID: {application_id}") - # Проверяем роль пользователя - cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) - user = cursor.fetchone() + with get_db() as conn: + cursor = conn.cursor() - stats = {} + # Сначала проверим, какие колонки есть в таблице + cursor.execute("PRAGMA table_info(applications)") + columns = [col[1] for col in cursor.fetchall()] + print(f"📊 Доступные колонки: {columns}") - if user["role"] == "employer": - # Статистика для работодателя (полученные отклики) - cursor.execute(""" + # Динамически строим запрос в зависимости от наличия колонок + select_fields = """ + a.*, + v.title as vacancy_title, + v.description as vacancy_description, + v.salary as vacancy_salary, + v.contact as vacancy_contact, + v.user_id as employer_id, + 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 + """ + + cursor.execute(f""" 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 + {select_fields} FROM applications a JOIN vacancies v ON a.vacancy_id = v.id - WHERE v.user_id = ? - """, (user_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,)) - 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,)) + application = cursor.fetchone() + if not application: + raise HTTPException(status_code=404, detail="Application not found") - stats = dict(cursor.fetchone()) - return stats + app_dict = dict(application) + + # Проверка прав доступа + if app_dict["user_id"] != user_id and app_dict["employer_id"] != user_id: + raise HTTPException(status_code=403, detail="Нет доступа к этому отклику") + + # Если просматривает работодатель и статус 'pending', меняем на 'viewed' + # Проверяем, существует ли колонка viewed_at + if 'viewed_at' in columns and app_dict["employer_id"] == user_id and app_dict["status"] == 'pending': + cursor.execute(""" + UPDATE applications + SET status = 'viewed', viewed_at = CURRENT_TIMESTAMP + WHERE id = ? + """, (application_id,)) + conn.commit() + app_dict["status"] = 'viewed' + + return app_dict + + except HTTPException: + raise + except Exception as e: + print(f"❌ Error in get_application: {e}") + import traceback + traceback.print_exc() + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") # ========== АДМИНСКИЕ ЭНДПОИНТЫ ========== @@ -2072,6 +2559,60 @@ async def get_user_contact( return dict(user) +@app.get("/api/health") +async def health_check(): + """Проверка работоспособности API""" + return { + "status": "ok", + "time": datetime.now().isoformat(), + "server": "Rabota.Today API" + } + + +# Улучшенная настройка CORS для мобильных устройств +# Настройка CORS - максимально разрешительная для мобильных +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Разрешить всем + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=["yarmarka.rabota.today", "localhost", "127.0.0.1"] +) + + +# Добавьте middleware для обработки мобильных User-Agent +@app.middleware("http") +async def detect_mobile(request: Request, call_next): + """Определение мобильных устройств и добавление заголовков""" + user_agent = request.headers.get("user-agent", "").lower() + + # Определяем мобильное устройство + is_mobile = any([ + "mobile" in user_agent, + "android" in user_agent, + "iphone" in user_agent, + "ipad" in user_agent, + "windows phone" in user_agent + ]) + + # Добавляем информацию о мобильности в request.state + request.state.is_mobile = is_mobile + + response = await call_next(request) + + # Добавляем заголовки для мобильных устройств + if is_mobile: + response.headers["X-Device-Type"] = "mobile" + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + + return response + + # Инициализация базы данных init_db() set_initial_sequence_ids() diff --git a/templates/admin.html b/templates/admin.html index 513429b..aa8214e 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -441,7 +441,9 @@ \ No newline at end of file diff --git a/templates/applications.html b/templates/applications.html index f51edf1..d9bb3a2 100644 --- a/templates/applications.html +++ b/templates/applications.html @@ -636,7 +636,9 @@
+ + \ No newline at end of file diff --git a/templates/debug.html b/templates/debug.html new file mode 100644 index 0000000..5346b8b --- /dev/null +++ b/templates/debug.html @@ -0,0 +1,27 @@ + + + + Debug API + + + +

Debug API

+ +

+
+    
+
+
diff --git a/templates/index.html b/templates/index.html
index 9cc7719..590b7ae 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -4,7 +4,6 @@
     
     
     Rabota.Today - Ярмарка вакансий
-    
     
     
 
 
     
+
- +

Найди работу мечты

-

Тысячи вакансий и резюме на одной платформе

+

Тысячи вакансий от ведущих компаний и резюме лучших специалистов на одной платформе

+
-
+
-
1,234
-
активных вакансий
+
0
+
активных вакансий
+
-
+
-
5,678
-
соискателей
+
0
+
соискателей
+
-
+
-
456
-
компаний
+
0
+
компаний
+
+ + +
+

Горячие вакансии

+ Все вакансии +
+
+
Загрузка вакансий...
+
+ + +
+

Новые резюме

+ Все резюме +
+
+
Загрузка резюме...
+
+ + +
+

Почему выбирают Rabota.Today

+
+
+ +

Быстрый отклик

+

Мгновенное уведомление работодателя о вашем интересе

+
+
+ +

Безопасность

+

Ваши персональные данные надежно защищены

+
+
+ +

Удобный доступ

+

Работает на всех устройствах — компьютер, планшет, телефон

+
+
+ +

Бесплатно

+

Для соискателей все функции абсолютно бесплатны

+
+
+
+ + +
+

Готовы начать?

+

Присоединяйтесь к тысячам соискателей и работодателей уже сегодня

+ + Создать аккаунт + +
+ + +
- + +
- - + const nav = document.getElementById('nav'); + const firstName = currentUser.full_name.split(' ')[0]; + const adminBadge = currentUser.is_admin ? 'Admin' : ''; + + nav.innerHTML = ` + Главная + Вакансии + Резюме + Избранное + Отклики + + `; + } + + // Загрузка статистики + async function loadStats() { + try { + // Загружаем общую статистику с сервера + const statsResponse = await fetch(`${API_BASE_URL}/public/stats`); + + if (statsResponse.ok) { + const stats = await statsResponse.json(); + + // Анимируем цифры + animateNumber(document.getElementById('vacanciesCount'), stats.active_vacancies || 1234); + animateNumber(document.getElementById('resumesCount'), stats.total_resumes || 5678); + animateNumber(document.getElementById('companiesCount'), stats.total_employers || 500); + + // Обновляем текст в CTA секции + if (stats.total_users) { + document.getElementById('totalUsers').textContent = formatNumber(stats.total_users) + ' ' + + (stats.total_users > 1000 ? 'тысяч' : ''); + } + } else { + // Если статистика не загрузилась, используем данные из API + const [vacResponse, resResponse] = await Promise.all([ + fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=1`), + fetch(`${API_BASE_URL}/resumes/all?page=1&limit=1`) + ]); + + const vacData = await vacResponse.json(); + const resData = await resResponse.json(); + + animateNumber(document.getElementById('vacanciesCount'), vacData.total || 1234); + animateNumber(document.getElementById('resumesCount'), resData.total || 5678); + document.getElementById('companiesCount').textContent = '500+'; + } + + } catch (error) { + console.error('Error loading stats:', error); + // Заглушки на случай ошибки + document.getElementById('vacanciesCount').textContent = '1,234'; + document.getElementById('resumesCount').textContent = '5,678'; + document.getElementById('companiesCount').textContent = '500+'; + } + } + + // Загрузка последних вакансий + async function loadRecentVacancies() { + try { + const response = await fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=3`); + const data = await response.json(); + + const container = document.getElementById('recentVacancies'); + + if (!data.vacancies || data.vacancies.length === 0) { + container.innerHTML = '
Нет активных вакансий
'; + return; + } + + container.innerHTML = data.vacancies.map(v => ` +
+
${escapeHtml(v.title)}
+
+ ${escapeHtml(v.company_name || 'Компания')} +
+
${escapeHtml(v.salary || 'Зарплата не указана')}
+
+ ${(v.tags || []).map(t => `${escapeHtml(t.name)}`).join('')} +
+ +
+ `).join(''); + + } catch (error) { + console.error('Error loading vacancies:', error); + document.getElementById('recentVacancies').innerHTML = '
Ошибка загрузки вакансий
'; + } + } + + // Загрузка последних резюме + async function loadRecentResumes() { + try { + const response = await fetch(`${API_BASE_URL}/resumes/all?page=1&limit=3`); + const data = await response.json(); + + const container = document.getElementById('recentResumes'); + + if (!data.resumes || data.resumes.length === 0) { + container.innerHTML = '
Нет резюме
'; + return; + } + + container.innerHTML = data.resumes.map(r => ` +
+
${escapeHtml(r.full_name)}
+
+ ${escapeHtml(r.desired_position || 'Должность не указана')} +
+
${escapeHtml(r.desired_salary || 'Зарплата не указана')}
+
+ ${(r.tags || []).map(t => `${escapeHtml(t.name)}`).join('')} +
+ +
+ `).join(''); + + } catch (error) { + console.error('Error loading resumes:', error); + document.getElementById('recentResumes').innerHTML = '
Ошибка загрузки резюме
'; + } + } + + // Загрузка трендов (изменение за последний месяц) + async function loadTrends() { + try { + // Здесь можно добавить логику для загрузки трендов + // Например, сравнивать с данными месяц назад + } catch (error) { + console.error('Error loading trends:', error); + } + } + + // Экранирование HTML + function escapeHtml(unsafe) { + if (!unsafe) return ''; + return unsafe.toString() + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + // Выход + function logout() { + localStorage.removeItem('accessToken'); + window.location.reload(); + } + + // Обновление статистики в реальном времени (каждые 30 секунд) + function startAutoRefresh() { + setInterval(() => { + loadStats(); + loadRecentVacancies(); + loadRecentResumes(); + }, 30000); + } + + // Инициализация + window.addEventListener('load', () => { + checkAuth(); + loadStats(); + loadRecentVacancies(); + loadRecentResumes(); + loadTrends(); + startAutoRefresh(); + }); + \ No newline at end of file diff --git a/templates/index_mobile.html b/templates/index_mobile.html new file mode 100644 index 0000000..f61041f --- /dev/null +++ b/templates/index_mobile.html @@ -0,0 +1,947 @@ + + + + + + Rabota.Today - Ярмарка вакансий + + + + +
+ + + + +
+

Найди работу мечты

+

Тысячи вакансий и резюме на одной платформе

+ +
+ + +
+
+ +
0
+
вакансий
+
+
+ +
0
+
резюме
+
+
+ +
0
+
компаний
+
+
+ + +
+

Горячие вакансии

+ Все → +
+
+
Загрузка...
+
+ + +
+

Новые резюме

+ Все → +
+
+
Загрузка...
+
+ + +
+

Почему выбирают нас

+
+
+ +

Быстро

+

Мгновенный отклик

+
+
+ +

Безопасно

+

Ваши данные под защитой

+
+
+ +

Удобно

+

Работает на всех устройствах

+
+
+ +

Бесплатно

+

Для соискателей

+
+
+
+ + +
+

Готовы начать?

+

Присоединяйтесь к тысячам пользователей

+ + Создать аккаунт + +
+ + + +
+ + +
+ + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index f3b06ad..702e29b 100644 --- a/templates/login.html +++ b/templates/login.html @@ -2,321 +2,288 @@ - + Вход | Rabota.Today -
-
+