- 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>
246 lines
9.3 KiB
Python
246 lines
9.3 KiB
Python
"""API для управления чек-листами."""
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile
|
||
from sqlalchemy.orm import Session
|
||
import csv
|
||
import io
|
||
|
||
from app.api.deps import get_current_user, require_roles
|
||
from app.db.session import get_db
|
||
from app.models import ChecklistTemplate, ChecklistItem
|
||
from app.schemas.audit import (
|
||
ChecklistTemplateCreate, ChecklistTemplateOut,
|
||
ChecklistItemCreate, ChecklistItemOut
|
||
)
|
||
|
||
router = APIRouter(tags=["checklists"])
|
||
|
||
|
||
@router.get("/checklists/templates", response_model=list[ChecklistTemplateOut])
|
||
def list_templates(
|
||
domain: str | None = None,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Список шаблонов чек-листов."""
|
||
q = db.query(ChecklistTemplate).filter(ChecklistTemplate.is_active == True)
|
||
if domain:
|
||
q = q.filter(ChecklistTemplate.domain == domain)
|
||
templates = q.order_by(ChecklistTemplate.name, ChecklistTemplate.version.desc()).all()
|
||
result = []
|
||
for t in templates:
|
||
items = db.query(ChecklistItem).filter(ChecklistItem.template_id == t.id).order_by(ChecklistItem.sort_order).all()
|
||
out = ChecklistTemplateOut.model_validate(t)
|
||
out.items = [ChecklistItemOut.model_validate(i) for i in items]
|
||
result.append(out)
|
||
return result
|
||
|
||
|
||
@router.post(
|
||
"/checklists/templates",
|
||
response_model=ChecklistTemplateOut,
|
||
dependencies=[Depends(require_roles("admin", "authority_inspector"))],
|
||
)
|
||
def create_template(
|
||
payload: ChecklistTemplateCreate,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Создаёт новый шаблон чек-листа."""
|
||
template = ChecklistTemplate(
|
||
name=payload.name,
|
||
version=payload.version,
|
||
description=payload.description,
|
||
domain=payload.domain
|
||
)
|
||
db.add(template)
|
||
db.flush()
|
||
|
||
if payload.items:
|
||
for item_data in payload.items:
|
||
item = ChecklistItem(
|
||
template_id=template.id,
|
||
code=item_data.code,
|
||
text=item_data.text,
|
||
domain=item_data.domain,
|
||
sort_order=item_data.sort_order
|
||
)
|
||
db.add(item)
|
||
|
||
db.commit()
|
||
db.refresh(template)
|
||
|
||
out = ChecklistTemplateOut.model_validate(template)
|
||
if payload.items:
|
||
items = db.query(ChecklistItem).filter(ChecklistItem.template_id == template.id).all()
|
||
out.items = [ChecklistItemOut.model_validate(i) for i in items]
|
||
return out
|
||
|
||
|
||
@router.post(
|
||
"/checklists/generate",
|
||
response_model=ChecklistTemplateOut,
|
||
dependencies=[Depends(require_roles("admin", "authority_inspector"))],
|
||
)
|
||
def generate_checklist(
|
||
source: str, # "fap_m_inspection" | "ata" | "custom"
|
||
name: str,
|
||
items: list[ChecklistItemCreate] | None = None,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Автоматически генерирует чек-лист из предустановленного шаблона или пользовательских данных."""
|
||
template = ChecklistTemplate(name=name, version=1, domain=source)
|
||
db.add(template)
|
||
db.flush()
|
||
|
||
if source == "fap_m_inspection":
|
||
# Предустановленный шаблон для проверки по ФАП-М
|
||
preset_items = [
|
||
ChecklistItemCreate(code="M.A.301", text="ВС имеет действующий сертификат лётной годности", domain="ФАП-М"),
|
||
ChecklistItemCreate(code="M.A.302", text="ВС эксплуатируется в соответствии с CAME", domain="ФАП-М"),
|
||
ChecklistItemCreate(code="M.A.303", text="Выполнены все требования по техническому обслуживанию", domain="ФАП-М"),
|
||
ChecklistItemCreate(code="M.A.304", text="Все дефекты устранены или имеют действующие разрешения на полёты", domain="ФАП-М"),
|
||
ChecklistItemCreate(code="M.A.305", text="Документация по ВС актуальна и соответствует требованиям", domain="ФАП-М"),
|
||
]
|
||
items = preset_items
|
||
elif source == "custom" and items:
|
||
pass # Используем переданные items
|
||
else:
|
||
raise HTTPException(status_code=400, detail="Неверный source или отсутствуют items")
|
||
|
||
for item_data in items:
|
||
item = ChecklistItem(
|
||
template_id=template.id,
|
||
code=item_data.code,
|
||
text=item_data.text,
|
||
domain=item_data.domain,
|
||
sort_order=item_data.sort_order
|
||
)
|
||
db.add(item)
|
||
|
||
db.commit()
|
||
db.refresh(template)
|
||
|
||
out = ChecklistTemplateOut.model_validate(template)
|
||
items_db = db.query(ChecklistItem).filter(ChecklistItem.template_id == template.id).all()
|
||
out.items = [ChecklistItemOut.model_validate(i) for i in items_db]
|
||
return out
|
||
|
||
|
||
@router.get("/checklists/templates/{template_id}", response_model=ChecklistTemplateOut)
|
||
def get_template(
|
||
template_id: str,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Получает шаблон с пунктами."""
|
||
template = db.query(ChecklistTemplate).filter(ChecklistTemplate.id == template_id).first()
|
||
if not template:
|
||
raise HTTPException(status_code=404, detail="Шаблон не найден")
|
||
|
||
items = db.query(ChecklistItem).filter(ChecklistItem.template_id == template_id).order_by(ChecklistItem.sort_order).all()
|
||
out = ChecklistTemplateOut.model_validate(template)
|
||
out.items = [ChecklistItemOut.model_validate(i) for i in items]
|
||
return out
|
||
|
||
|
||
@router.post(
|
||
"/checklists/generate-from-csv",
|
||
response_model=ChecklistTemplateOut,
|
||
dependencies=[Depends(require_roles("admin", "authority_inspector"))],
|
||
)
|
||
async def generate_checklist_from_csv(
|
||
file: UploadFile = File(...),
|
||
name: str | None = None,
|
||
domain: str | None = None,
|
||
db: Session = Depends(get_db),
|
||
user=Depends(get_current_user),
|
||
):
|
||
"""Генерирует чек-лист из CSV файла (например, из REFLY_Jira_Backlog_Subtasks_Dependencies.csv).
|
||
|
||
Ожидаемые колонки в CSV:
|
||
- Issue Id (или Issue Id) - код пункта
|
||
- Summary (или Description) - текст требования
|
||
- Domain (опционально) - домен
|
||
- Story Points (опционально) - для sort_order
|
||
"""
|
||
content = await file.read()
|
||
|
||
# Парсим CSV
|
||
try:
|
||
text = content.decode('utf-8-sig')
|
||
except UnicodeDecodeError:
|
||
text = content.decode('cp1251')
|
||
|
||
reader = csv.DictReader(io.StringIO(text))
|
||
fieldnames = reader.fieldnames or []
|
||
rows = list(reader)
|
||
|
||
if not rows:
|
||
raise HTTPException(status_code=400, detail="CSV файл пуст или неверного формата")
|
||
|
||
# Определяем колонки (поддерживаем разные варианты названий)
|
||
code_col = None
|
||
text_col = None
|
||
domain_col = None
|
||
order_col = None
|
||
|
||
for col in fieldnames:
|
||
col_lower = col.lower()
|
||
if 'issue' in col_lower and 'id' in col_lower and not code_col:
|
||
code_col = col
|
||
elif ('summary' in col_lower or 'description' in col_lower) and not text_col:
|
||
text_col = col
|
||
elif 'domain' in col_lower and not domain_col:
|
||
domain_col = col
|
||
elif ('story' in col_lower and 'point' in col_lower) or 'order' in col_lower:
|
||
order_col = col
|
||
|
||
if not code_col or not text_col:
|
||
raise HTTPException(status_code=400, detail="Не найдены обязательные колонки: Issue Id и Summary/Description")
|
||
|
||
# Создаём шаблон
|
||
template_name = name or file.filename.replace('.csv', '').replace('_', ' ').title()
|
||
template = ChecklistTemplate(
|
||
name=template_name,
|
||
version=1,
|
||
domain=domain or "REFLY_CSV"
|
||
)
|
||
db.add(template)
|
||
db.flush()
|
||
|
||
# Создаём пункты
|
||
items_created = []
|
||
for idx, row in enumerate(rows):
|
||
code = str(row.get(code_col, f"ITEM_{idx+1}")).strip()
|
||
text = str(row.get(text_col, "")).strip()
|
||
|
||
if not code or not text:
|
||
continue # Пропускаем пустые строки
|
||
|
||
item_domain = domain or (row.get(domain_col, "") if domain_col else None) or None
|
||
sort_order = int(row.get(order_col, idx + 1)) if order_col and row.get(order_col) else idx + 1
|
||
|
||
item = ChecklistItem(
|
||
template_id=template.id,
|
||
code=code,
|
||
text=text,
|
||
domain=item_domain,
|
||
sort_order=sort_order
|
||
)
|
||
db.add(item)
|
||
items_created.append(item)
|
||
|
||
if not items_created:
|
||
db.rollback()
|
||
raise HTTPException(status_code=400, detail="Не удалось создать ни одного пункта из CSV")
|
||
|
||
db.commit()
|
||
db.refresh(template)
|
||
|
||
out = ChecklistTemplateOut.model_validate(template)
|
||
items_db = db.query(ChecklistItem).filter(ChecklistItem.template_id == template.id).order_by(ChecklistItem.sort_order).all()
|
||
out.items = [ChecklistItemOut.model_validate(i) for i in items_db]
|
||
return out
|