140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
from io import BytesIO
|
|
import re
|
|
import xml.etree.ElementTree as ET
|
|
import zipfile
|
|
|
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
|
|
|
from backend.database import db
|
|
from backend.routers.auth import require_roles
|
|
from backend.schemas import CaseListResponse, CaseResponse, CaseUpdate, DashboardResponse, ParseDocResponse
|
|
|
|
router = APIRouter(prefix='/api/v1/admin', tags=['admin'], dependencies=[Depends(require_roles(['admin']))])
|
|
|
|
|
|
def _parse_bool(value: str | None) -> bool | None:
|
|
if value is None:
|
|
return None
|
|
normalized = value.strip().lower()
|
|
if normalized in {'1', 'true', 'yes', 'y', 'да'}:
|
|
return True
|
|
if normalized in {'0', 'false', 'no', 'n', 'нет'}:
|
|
return False
|
|
return None
|
|
|
|
|
|
def _extract_docx_text(content: bytes) -> str:
|
|
with zipfile.ZipFile(BytesIO(content)) as archive:
|
|
xml = archive.read('word/document.xml')
|
|
root = ET.fromstring(xml)
|
|
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
|
|
paragraphs: list[str] = []
|
|
for paragraph in root.findall('.//w:body/w:p', ns):
|
|
texts = [node.text for node in paragraph.findall('.//w:t', ns) if node.text]
|
|
if texts:
|
|
paragraphs.append(''.join(texts).strip())
|
|
return '\n'.join(paragraphs).strip()
|
|
|
|
|
|
def _extract_kv_pairs(raw_text: str) -> dict[str, str]:
|
|
parsed: dict[str, str] = {}
|
|
for line in raw_text.splitlines():
|
|
if ':' not in line:
|
|
continue
|
|
key, value = line.split(':', 1)
|
|
parsed[key.strip().lower()] = value.strip()
|
|
return parsed
|
|
|
|
|
|
def _coerce_preview(raw_text: str) -> dict:
|
|
pairs = _extract_kv_pairs(raw_text)
|
|
case: dict[str, object] = {}
|
|
|
|
if 'age' in pairs:
|
|
try:
|
|
case['age'] = int(pairs['age'])
|
|
except ValueError:
|
|
pass
|
|
if 'gender' in pairs:
|
|
case['gender'] = pairs['gender']
|
|
if 'status' in pairs:
|
|
case['status'] = pairs['status']
|
|
if 'found alive' in pairs:
|
|
case['found_alive'] = _parse_bool(pairs['found alive'])
|
|
if 'found distance km' in pairs:
|
|
try:
|
|
case['found_distance_km'] = float(pairs['found distance km'])
|
|
except ValueError:
|
|
pass
|
|
if 'found lat' in pairs:
|
|
try:
|
|
case['found_lat'] = float(pairs['found lat'])
|
|
except ValueError:
|
|
pass
|
|
if 'found lon' in pairs:
|
|
try:
|
|
case['found_lon'] = float(pairs['found lon'])
|
|
except ValueError:
|
|
pass
|
|
if 'who found' in pairs:
|
|
case['who_found'] = pairs['who found']
|
|
if 'notes' in pairs:
|
|
case['notes'] = pairs['notes']
|
|
elif 'note' in pairs:
|
|
case['notes'] = pairs['note']
|
|
|
|
case_id = None
|
|
if 'case id' in pairs:
|
|
match = re.search(r'\d+', pairs['case id'])
|
|
if match:
|
|
case_id = match.group(0)
|
|
|
|
return {'raw_text': raw_text, 'case_id': case_id, 'case': case}
|
|
|
|
|
|
@router.get('/cases', response_model=CaseListResponse)
|
|
def admin_cases(
|
|
page: int = Query(default=1, ge=1),
|
|
page_size: int = Query(default=20, ge=1, le=100),
|
|
status: str | None = None,
|
|
age_min: int | None = Query(default=None, ge=0),
|
|
age_max: int | None = Query(default=None, ge=0),
|
|
) -> dict:
|
|
cases = db.list_cases()
|
|
if status:
|
|
cases = [c for c in cases if c.status == status]
|
|
if age_min is not None:
|
|
cases = [c for c in cases if c.age is not None and c.age >= age_min]
|
|
if age_max is not None:
|
|
cases = [c for c in cases if c.age is not None and c.age <= age_max]
|
|
start = (page - 1) * page_size
|
|
end = start + page_size
|
|
return {'items': [c.to_detail() for c in cases[start:end]], 'total': len(cases), 'page': page, 'page_size': page_size}
|
|
|
|
|
|
@router.patch('/cases/{case_id}', response_model=CaseResponse)
|
|
def admin_update_case(case_id: str, payload: CaseUpdate) -> dict:
|
|
data = payload.model_dump(exclude_none=True)
|
|
note = data.pop('note', None)
|
|
if note is not None and 'notes' not in data:
|
|
data['notes'] = note
|
|
case = db.update_case(case_id, **data)
|
|
if not case:
|
|
raise HTTPException(status_code=404, detail='Case not found')
|
|
return case.to_detail()
|
|
|
|
|
|
@router.get('/dashboard', response_model=DashboardResponse)
|
|
def admin_dashboard() -> dict:
|
|
return db.stats()
|
|
|
|
|
|
@router.post('/parse-doc', response_model=ParseDocResponse)
|
|
async def admin_parse_doc(file: UploadFile = File(...)) -> dict:
|
|
content = await file.read()
|
|
raw_text = _extract_docx_text(content)
|
|
preview = _coerce_preview(raw_text)
|
|
return {'filename': file.filename, 'parsed': True, 'preview': preview, 'raw_text': raw_text}
|