diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 299e992..558b6ed 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -1,107 +1,19 @@ from __future__ import annotations -from io import BytesIO -import re -import xml.etree.ElementTree as ET -import zipfile - from datetime import datetime, timezone import logging -from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile +from fastapi import APIRouter, Depends, HTTPException, Query from backend.database import db from backend.routers.auth import require_roles -from backend.schemas import CaseListResponse, CaseResponse, CaseUpdate, DashboardResponse, ParseDocResponse +from backend.schemas import CaseListResponse, CaseResponse, CaseUpdate, DashboardResponse log = logging.getLogger('vector.admin') 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: - try: - with zipfile.ZipFile(BytesIO(content)) as archive: - xml = archive.read('word/document.xml') - root = ET.fromstring(xml) - except (zipfile.BadZipFile, KeyError, ET.ParseError) as exc: - raise ValueError('Не удалось прочитать .docx: файл повреждён или имеет неверный формат') from exc - 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), @@ -157,14 +69,3 @@ def admin_update_case(case_id: str, payload: CaseUpdate) -> dict: @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() - try: - raw_text = _extract_docx_text(content) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - preview = _coerce_preview(raw_text) - return {'filename': file.filename, 'parsed': True, 'preview': preview, 'raw_text': raw_text} diff --git a/backend/schemas.py b/backend/schemas.py index f2ae606..4fc59a5 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -135,13 +135,6 @@ class AnalyzeResponse(BaseModel): case_id: str -class ParseDocResponse(BaseModel): - filename: str - parsed: bool - preview: dict[str, Any] - raw_text: str | None = None - - # ================= ЗАВЕРШЁННЫЕ ПОИСКИ (ручной ввод, без ИИ) ================= # Оператор руками вносит данные ЗАВЕРШЁННОГО поиска: параметры субъекта/среды # (вход матмодели) + реальный исход. Цель — датасет для калибровки модели diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index 006584f..2c42e44 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -148,14 +148,6 @@ function AdminDashboard() { const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(''); const [saveMessage, setSaveMessage] = useState(''); - const [uploadFile, setUploadFile] = useState(null); - const [uploadLoading, setUploadLoading] = useState(false); - const [uploadError, setUploadError] = useState(''); - const [uploadMessage, setUploadMessage] = useState(''); - const [parsePreview, setParsePreview] = useState(null); - const [parseRawText, setParseRawText] = useState(''); - const [parseCaseId, setParseCaseId] = useState(''); - const [parseCaseFields, setParseCaseFields] = useState({}); const selectedCase = useMemo( () => cases.find((item) => item.id === selectedCaseId) || null, @@ -240,116 +232,6 @@ function AdminDashboard() { const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); - const parsePreviewFields = useMemo(() => { - if (!parsePreview) return []; - return Object.entries(parsePreview).filter(([key]) => key !== 'case' && key !== 'raw_text'); - }, [parsePreview]); - - function resetParseState() { - setUploadFile(null); - setUploadError(''); - setUploadMessage(''); - setParsePreview(null); - setParseRawText(''); - setParseCaseId(''); - setParseCaseFields({}); - } - - function handleUploadFile(event) { - setUploadFile(event.target.files?.[0] || null); - setUploadError(''); - setUploadMessage(''); - } - - async function handleUploadPreview(event) { - event.preventDefault(); - if (!uploadFile) { - setUploadError('Выбери .docx файл для предпросмотра'); - return; - } - - setUploadLoading(true); - setUploadError(''); - setUploadMessage(''); - - try { - const formData = new FormData(); - formData.append('file', uploadFile); - - const response = await apiFetch('/api/v1/admin/parse-doc', { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - const body = await response.json().catch(() => ({})); - throw new Error(body.detail || `Не удалось распарсить документ (${response.status})`); - } - - const payload = await response.json(); - setParsePreview(payload.preview || {}); - setParseRawText(payload.raw_text || ''); - setParseCaseId(payload.preview?.case_id || ''); - setParseCaseFields(payload.preview?.case || {}); - setUploadMessage('Документ распознан, можно проверить preview ниже'); - } catch (err) { - setUploadError(err.message || 'Ошибка загрузки файла'); - } finally { - setUploadLoading(false); - } - } - - function fieldFromPreview(name, fallback = '') { - const value = parseCaseFields?.[name]; - return value === undefined || value === null || value === '' ? fallback : String(value); - } - - async function handleImportPreview(event) { - event.preventDefault(); - if (!parseCaseFields || Object.keys(parseCaseFields).length === 0) { - setUploadError('Сначала загрузи и распарсь .docx файл'); - return; - } - - setUploadLoading(true); - setUploadError(''); - setUploadMessage(''); - - try { - const payload = { - age: parseCaseFields.age ?? null, - gender: parseCaseFields.gender ?? null, - status: parseCaseFields.status ?? 'new', - found_alive: parseCaseFields.found_alive ?? null, - found_distance_km: parseCaseFields.found_distance_km ?? null, - found_lat: parseCaseFields.found_lat ?? null, - found_lon: parseCaseFields.found_lon ?? null, - who_found: parseCaseFields.who_found ?? null, - note: parseCaseFields.notes ?? null, - }; - - const response = await apiFetch('/api/v1/cases', { - method: 'POST', - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const body = await response.json().catch(() => ({})); - throw new Error(body.detail || `Не удалось создать кейс (${response.status})`); - } - - const created = await response.json(); - setUploadMessage(`Кейс создан: ${created.id}`); - resetParseState(); - setSaveMessage('Импорт из .docx завершён'); - setPage(1); - } catch (err) { - setUploadError(err.message || 'Ошибка импорта'); - } finally { - setUploadLoading(false); - } - } - function handleFilterChange(event) { const { name, value } = event.target; setPage(1); @@ -560,71 +442,6 @@ function AdminDashboard() { -
-
-
-

Импорт отчёта .docx

-

Загрузите документ, проверьте preview и импортируйте извлечённые поля в новый кейс.

-
-
- {parseCaseId ? `Preview case_id: ${parseCaseId}` : 'Ждём файл'} -
-
- -
-
- - - -
-
- - {uploadError ?
{uploadError}
: null} - {uploadMessage ?
{uploadMessage}
: null} - - {parsePreview ? ( -
-
-
-

Распознанные поля

-
    - {parsePreviewFields.map(([key, value]) => ( -
  • - {key} - {typeof value === 'object' ? JSON.stringify(value) : String(value)} -
  • - ))} -
-
-
-

Кейс preview

-
    -
  • age{fieldFromPreview('age', '—')}
  • -
  • gender{fieldFromPreview('gender', '—')}
  • -
  • status{fieldFromPreview('status', 'new')}
  • -
  • found_alive{fieldFromPreview('found_alive', '—')}
  • -
  • found_distance_km{fieldFromPreview('found_distance_km', '—')}
  • -
  • who_found{fieldFromPreview('who_found', '—')}
  • -
  • notes{fieldFromPreview('notes', '—')}
  • -
-
-
- {parseRawText ? ( -
- Показать raw text -
{parseRawText}
-
- ) : null} -
- ) : null} -
{error ?
{error}
: null}