from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import desc from typing import List, Optional from uuid import UUID from database import get_db from models import Case, User from schemas import CaseCreate, CaseOut from pydantic import BaseModel # Импортируем auth dependencies import sys sys.path.append('/app/api/v1') from auth import get_current_user, require_role router = APIRouter() class CaseUpdate(BaseModel): """Schema for updating case fields""" # Ребёнок child_name: Optional[str] = None age_years: Optional[int] = None gender: Optional[str] = None height_build: Optional[str] = None clothes_upper: Optional[str] = None clothes_lower: Optional[str] = None shoes: Optional[str] = None clothes_description: Optional[str] = None special_marks: Optional[str] = None phone_status: Optional[str] = None # Здоровье has_diagnosis: Optional[bool] = None diagnosis_type: Optional[List[str]] = None fitness_level: Optional[str] = None has_transport: Optional[str] = None cant_swim: Optional[bool] = None # Психотип psychotype: Optional[str] = None psychotype_answers: Optional[dict] = None # Обстоятельства loss_reason: Optional[str] = None loss_time: Optional[str] = None elapsed_hours: Optional[float] = None last_seen_direction: Optional[str] = None last_seen_reliability: Optional[str] = None last_seen_description: Optional[str] = None behavior_description: Optional[str] = None familiar_places: Optional[str] = None lost_before: Optional[str] = None # Среда season: Optional[str] = None temperature_c: Optional[float] = None precipitation: Optional[str] = None visibility: Optional[str] = None wind: Optional[str] = None terrain: Optional[List[str]] = None # GPS tnp_lat: Optional[float] = None tnp_lon: Optional[float] = None tnp_address: Optional[str] = None # Ресурсы teams_count: Optional[int] = None team_size: Optional[int] = None has_dog: Optional[bool] = None extra_resources: Optional[List[str]] = None # Исход found_alive: Optional[bool] = None found_distance_km: Optional[float] = None found_direction: Optional[str] = None found_location_type: Optional[str] = None found_lat: Optional[float] = None found_lon: Optional[float] = None search_duration_hours: Optional[float] = None who_found: Optional[str] = None # Статус status: Optional[str] = None class CaseListResponse(BaseModel): """Response for list endpoint with pagination""" total: int skip: int limit: int cases: List[CaseOut] @router.post("/cases", response_model=CaseOut, status_code=201) async def create_case( case_data: CaseCreate, db: Session = Depends(get_db) ): """ Создать новый случай поиска. Принимает все поля из формы опроса (5 шагов). Публичный эндпоинт - не требует аутентификации. """ db_case = Case(**case_data.model_dump(exclude_unset=True)) db.add(db_case) db.commit() db.refresh(db_case) return db_case @router.get("/cases", response_model=CaseListResponse) async def list_cases( skip: int = Query(0, ge=0, description="Количество пропускаемых записей"), limit: int = Query(50, ge=1, le=100, description="Максимум записей на страницу"), status: Optional[str] = Query(None, description="Фильтр по статусу: active/closed/archived"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Получить список случаев с пагинацией и фильтрацией. - **skip**: смещение (для пагинации) - **limit**: количество записей (макс 100) - **status**: фильтр по статусу (active/closed/archived) Требуется аутентификация (operator, field, admin). """ query = db.query(Case) if status: query = query.filter(Case.status == status) total = query.count() cases = query.order_by(desc(Case.created_at)).offset(skip).limit(limit).all() return { "total": total, "skip": skip, "limit": limit, "cases": cases } @router.get("/cases/{case_id}", response_model=CaseOut) async def get_case( case_id: UUID, db: Session = Depends(get_db) ): """ Получить случай по ID. Возвращает все поля случая включая исход (если заполнен). Публичный эндпоинт - не требует аутентификации. """ case = db.query(Case).filter(Case.id == case_id).first() if not case: raise HTTPException(status_code=404, detail=f"Case {case_id} not found") return case @router.patch("/cases/{case_id}", response_model=CaseOut) async def update_case( case_id: UUID, case_update: CaseUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Обновить случай (частичное обновление). Используется для: - Корректировки данных опроса - Внесения исхода поиска (found_alive, found_distance_km и т.д.) - Изменения статуса (active → closed) Требуется аутентификация (operator, field, admin). """ case = db.query(Case).filter(Case.id == case_id).first() if not case: raise HTTPException(status_code=404, detail=f"Case {case_id} not found") update_data = case_update.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(case, field, value) db.commit() db.refresh(case) return case @router.delete("/cases/{case_id}", status_code=204) async def delete_case( case_id: UUID, db: Session = Depends(get_db), current_user: User = Depends(require_role(["admin"])) ): """ Удалить случай (только для admin). В продакшене рекомендуется использовать архивацию вместо удаления. """ case = db.query(Case).filter(Case.id == case_id).first() if not case: raise HTTPException(status_code=404, detail=f"Case {case_id} not found") db.delete(case) db.commit() return None