diff --git a/app/routes/documents.py b/app/routes/documents.py index 69a7093..56c6162 100644 --- a/app/routes/documents.py +++ b/app/routes/documents.py @@ -6,6 +6,7 @@ from pathlib import Path from fastapi import APIRouter, Depends, Form, Query, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates +from sqlalchemy import distinct from sqlalchemy.orm import Session, selectinload from app.db.deps import get_db @@ -257,6 +258,22 @@ def _apply_reviewed_lines_to_layout(base_layout: dict | None, reviewed_text: str return new_layout + +def _get_existing_document_types(db: Session) -> list[str]: + rows = ( + db.query(distinct(Document.document_type)) + .filter(Document.document_type.isnot(None)) + .order_by(Document.document_type.asc()) + .all() + ) + values: list[str] = [] + for row in rows: + value = row[0] + if value: + values.append(str(value)) + return values + + def _get_queue_navigation(db: Session, document: Document) -> dict: active_docs = ( db.query(Document) @@ -458,6 +475,23 @@ def list_documents( ) + +@router.post("/{document_id}/save-document-type", response_class=RedirectResponse) +def save_document_type_route( + document_id: str, + document_type: str = Form(""), + db: Session = Depends(get_db), +): + document = db.query(Document).filter(Document.document_id == document_id).first() + if document is None: + return RedirectResponse(url="/documents/", status_code=303) + + document.document_type = document_type.strip() or None + db.commit() + + return RedirectResponse(url=f"/documents/{document.document_id}", status_code=303) + + @router.post("/{document_id}/rerun-ocr", response_class=RedirectResponse) def rerun_ocr(document_id: str, db: Session = Depends(get_db)): document = db.query(Document).filter(Document.document_id == document_id).first() @@ -721,6 +755,7 @@ def document_detail(document_id: str, request: Request, queue: str | None = None selected_preset = _get_preset_by_id(db, preset_id) all_presets = _get_all_presets(db) + existing_document_types = _get_existing_document_types(db) extracted_form = _extracted_field_form_values(document, request) additional_form = _additional_field_form_values(document, selected_preset) @@ -762,6 +797,7 @@ def document_detail(document_id: str, request: Request, queue: str | None = None "current_additional": current_additional, "presets": all_presets, "selected_preset_id": preset_id, + "existing_document_types": existing_document_types, "active_tab": active_tab, "active_page": "documents", }, diff --git a/app/templates/documents/detail.html b/app/templates/documents/detail.html index ac426ac..d3f2e09 100644 --- a/app/templates/documents/detail.html +++ b/app/templates/documents/detail.html @@ -54,6 +54,26 @@ +
+
+
+ + + +
+
+
+ +
+
+
{% if prev_doc %} ← Previous @@ -432,6 +452,59 @@ syncScroll(); } })(); + + const documentTypeInput = document.getElementById("document_type_input"); + const documentTypeSuggestions = document.getElementById("document-type-suggestions"); + const existingDocumentTypes = {{ existing_document_types|tojson }}; + + if (documentTypeInput && documentTypeSuggestions) { + function renderDocumentTypeSuggestions() { + const value = (documentTypeInput.value || "").trim().toLowerCase(); + + let matches = existingDocumentTypes.filter(function (item) { + return item && (!value || item.toLowerCase().includes(value)); + }); + + if (value) { + matches = matches.filter(function (item) { + return item.toLowerCase() !== value; + }); + } + + matches = matches.slice(0, 8); + + if (!matches.length) { + documentTypeSuggestions.style.display = "none"; + documentTypeSuggestions.innerHTML = ""; + return; + } + + documentTypeSuggestions.innerHTML = matches.map(function (item) { + return ''; + }).join(""); + + documentTypeSuggestions.style.display = "block"; + + documentTypeSuggestions.querySelectorAll(".doc-type-option").forEach(function (btn) { + btn.addEventListener("click", function () { + documentTypeInput.value = btn.getAttribute("data-value") || ""; + documentTypeSuggestions.style.display = "none"; + documentTypeSuggestions.innerHTML = ""; + documentTypeInput.focus(); + }); + }); + } + + documentTypeInput.addEventListener("input", renderDocumentTypeSuggestions); + documentTypeInput.addEventListener("focus", renderDocumentTypeSuggestions); + + document.addEventListener("click", function (e) { + if (!documentTypeSuggestions.contains(e.target) && e.target !== documentTypeInput) { + documentTypeSuggestions.style.display = "none"; + } + }); + } +