- Unify API: lib/api.ts uses /api/v1, inbox uses /api/inbox (rewrites) - Remove localhost refs: openapi, inbox page - Add rewrites: /api/inbox|tmc -> inbox-server, /api/v1 -> FastAPI - Add stub routes: knowledge/insights, recommendations, search, log-error - Transfer from PAPA: prompts (inspection, tmc), scripts, supabase, data/tmc-requests - Fix inbox-server: ORDER BY created_at, package.json - Remove redundant app/api/inbox/files route (rewrites handle it) - knowledge/ in gitignore (large PDFs) Co-authored-by: Cursor <cursoragent@cursor.com>
218 lines
7.7 KiB
Python
218 lines
7.7 KiB
Python
"""API для проведения аудитов (проверок) по чек-листам."""
|
||
|
||
from datetime import datetime, timezone
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.api.deps import get_current_user, require_roles
|
||
from app.db.session import get_db
|
||
from app.models import Audit, AuditResponse, Finding, ChecklistTemplate, ChecklistItem, Aircraft
|
||
from app.schemas.audit import (
|
||
AuditCreate, AuditOut, AuditResponseCreate, AuditResponseOut, FindingOut
|
||
)
|
||
|
||
router = APIRouter(tags=["checklist-audits"])
|
||
|
||
|
||
@router.get("/audits", response_model=list[AuditOut])
|
||
def list_audits(
|
||
aircraft_id: str | None = None,
|
||
status: str | None = None,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Список аудитов."""
|
||
q = db.query(Audit)
|
||
|
||
if aircraft_id:
|
||
q = q.filter(Audit.aircraft_id == aircraft_id)
|
||
if status:
|
||
q = q.filter(Audit.status == status)
|
||
|
||
# Фильтр по организации для операторов
|
||
if user.role.startswith("operator") and user.organization_id:
|
||
q = q.join(Aircraft).filter(Aircraft.operator_id == user.organization_id)
|
||
|
||
audits = q.order_by(Audit.created_at.desc()).limit(200).all()
|
||
return [AuditOut.model_validate(a) for a in audits]
|
||
|
||
|
||
@router.post(
|
||
"/audits",
|
||
response_model=AuditOut,
|
||
dependencies=[Depends(require_roles("admin", "authority_inspector", "operator_manager"))],
|
||
)
|
||
def create_audit(
|
||
payload: AuditCreate,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Создаёт новый аудит."""
|
||
template = db.query(ChecklistTemplate).filter(ChecklistTemplate.id == payload.template_id).first()
|
||
if not template:
|
||
raise HTTPException(status_code=404, detail="Шаблон не найден")
|
||
|
||
aircraft = db.query(Aircraft).filter(Aircraft.id == payload.aircraft_id).first()
|
||
if not aircraft:
|
||
raise HTTPException(status_code=404, detail="ВС не найдено")
|
||
|
||
audit = Audit(
|
||
template_id=payload.template_id,
|
||
aircraft_id=payload.aircraft_id,
|
||
planned_at=payload.planned_at,
|
||
inspector_user_id=user.id,
|
||
status="draft"
|
||
)
|
||
db.add(audit)
|
||
db.commit()
|
||
db.refresh(audit)
|
||
return AuditOut.model_validate(audit)
|
||
|
||
|
||
@router.get("/audits/{audit_id}", response_model=AuditOut)
|
||
def get_audit(
|
||
audit_id: str,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Получает аудит с ответами и находками."""
|
||
audit = db.query(Audit).filter(Audit.id == audit_id).first()
|
||
if not audit:
|
||
raise HTTPException(status_code=404, detail="Аудит не найден")
|
||
|
||
return AuditOut.model_validate(audit)
|
||
|
||
|
||
@router.post(
|
||
"/audits/{audit_id}/responses",
|
||
response_model=AuditResponseOut,
|
||
dependencies=[Depends(require_roles("admin", "authority_inspector", "operator_manager"))],
|
||
)
|
||
def submit_response(
|
||
audit_id: str,
|
||
payload: AuditResponseCreate,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Отправляет ответ по пункту чек-листа. Автоматически создаёт Finding при non_compliant."""
|
||
audit = db.query(Audit).filter(Audit.id == audit_id).first()
|
||
if not audit:
|
||
raise HTTPException(status_code=404, detail="Аудит не найден")
|
||
|
||
if audit.status == "completed":
|
||
raise HTTPException(status_code=400, detail="Аудит уже завершён")
|
||
|
||
item = db.query(ChecklistItem).filter(ChecklistItem.id == payload.item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Пункт чек-листа не найден")
|
||
|
||
# Проверяем, нет ли уже ответа
|
||
existing = db.query(AuditResponse).filter(
|
||
AuditResponse.audit_id == audit_id,
|
||
AuditResponse.item_id == payload.item_id
|
||
).first()
|
||
|
||
if existing:
|
||
existing.answer = payload.answer
|
||
existing.comment = payload.comment
|
||
existing.evidence_attachment_ids = payload.evidence_attachment_ids
|
||
response = existing
|
||
else:
|
||
response = AuditResponse(
|
||
audit_id=audit_id,
|
||
item_id=payload.item_id,
|
||
answer=payload.answer,
|
||
comment=payload.comment,
|
||
evidence_attachment_ids=payload.evidence_attachment_ids
|
||
)
|
||
db.add(response)
|
||
|
||
# Автоматически создаём Finding при non_compliant
|
||
if payload.answer == "non_compliant":
|
||
existing_finding = db.query(Finding).filter(
|
||
Finding.audit_id == audit_id,
|
||
Finding.item_id == payload.item_id
|
||
).first()
|
||
|
||
if not existing_finding:
|
||
# Определяем severity и risk_score на основе типа пункта
|
||
severity = "high"
|
||
risk_score = 75
|
||
if "критич" in item.text.lower() or "безопасн" in item.text.lower():
|
||
severity = "critical"
|
||
risk_score = 100
|
||
elif "рекоменд" in item.text.lower():
|
||
severity = "medium"
|
||
risk_score = 50
|
||
|
||
finding = Finding(
|
||
audit_id=audit_id,
|
||
response_id=response.id if hasattr(response, 'id') else None,
|
||
item_id=payload.item_id,
|
||
severity=severity,
|
||
risk_score=risk_score,
|
||
status="open",
|
||
description=f"Несоответствие по пункту {item.code}: {item.text}"
|
||
)
|
||
db.add(finding)
|
||
|
||
# Обновляем статус аудита
|
||
if audit.status == "draft":
|
||
audit.status = "in_progress"
|
||
|
||
db.commit()
|
||
db.refresh(response)
|
||
return AuditResponseOut.model_validate(response)
|
||
|
||
|
||
@router.get("/audits/{audit_id}/responses", response_model=list[AuditResponseOut])
|
||
def list_responses(
|
||
audit_id: str,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Список ответов по аудиту."""
|
||
audit = db.query(Audit).filter(Audit.id == audit_id).first()
|
||
if not audit:
|
||
raise HTTPException(status_code=404, detail="Аудит не найден")
|
||
|
||
responses = db.query(AuditResponse).filter(AuditResponse.audit_id == audit_id).all()
|
||
return [AuditResponseOut.model_validate(r) for r in responses]
|
||
|
||
|
||
@router.get("/audits/{audit_id}/findings", response_model=list[FindingOut])
|
||
def list_findings(
|
||
audit_id: str,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Список находок (несоответствий) по аудиту."""
|
||
audit = db.query(Audit).filter(Audit.id == audit_id).first()
|
||
if not audit:
|
||
raise HTTPException(status_code=404, detail="Аудит не найден")
|
||
|
||
findings = db.query(Finding).filter(Finding.audit_id == audit_id).order_by(Finding.severity.desc()).all()
|
||
return [FindingOut.model_validate(f) for f in findings]
|
||
|
||
|
||
@router.patch(
|
||
"/audits/{audit_id}/complete",
|
||
response_model=AuditOut,
|
||
dependencies=[Depends(require_roles("admin", "authority_inspector"))],
|
||
)
|
||
def complete_audit(
|
||
audit_id: str,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Завершает аудит."""
|
||
audit = db.query(Audit).filter(Audit.id == audit_id).first()
|
||
if not audit:
|
||
raise HTTPException(status_code=404, detail="Аудит не найден")
|
||
|
||
audit.status = "completed"
|
||
audit.completed_at = datetime.now(timezone.utc)
|
||
db.commit()
|
||
db.refresh(audit)
|
||
return AuditOut.model_validate(audit)
|