feat: add editable document type with autocomplete suggestions
This commit is contained in:
parent
aa7f8d6a54
commit
f1df9c29bb
|
|
@ -6,6 +6,7 @@ from pathlib import Path
|
||||||
from fastapi import APIRouter, Depends, Form, Query, Request
|
from fastapi import APIRouter, Depends, Form, Query, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from sqlalchemy import distinct
|
||||||
from sqlalchemy.orm import Session, selectinload
|
from sqlalchemy.orm import Session, selectinload
|
||||||
|
|
||||||
from app.db.deps import get_db
|
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
|
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:
|
def _get_queue_navigation(db: Session, document: Document) -> dict:
|
||||||
active_docs = (
|
active_docs = (
|
||||||
db.query(Document)
|
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)
|
@router.post("/{document_id}/rerun-ocr", response_class=RedirectResponse)
|
||||||
def rerun_ocr(document_id: str, db: Session = Depends(get_db)):
|
def rerun_ocr(document_id: str, db: Session = Depends(get_db)):
|
||||||
document = db.query(Document).filter(Document.document_id == document_id).first()
|
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)
|
selected_preset = _get_preset_by_id(db, preset_id)
|
||||||
all_presets = _get_all_presets(db)
|
all_presets = _get_all_presets(db)
|
||||||
|
existing_document_types = _get_existing_document_types(db)
|
||||||
|
|
||||||
extracted_form = _extracted_field_form_values(document, request)
|
extracted_form = _extracted_field_form_values(document, request)
|
||||||
additional_form = _additional_field_form_values(document, selected_preset)
|
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,
|
"current_additional": current_additional,
|
||||||
"presets": all_presets,
|
"presets": all_presets,
|
||||||
"selected_preset_id": preset_id,
|
"selected_preset_id": preset_id,
|
||||||
|
"existing_document_types": existing_document_types,
|
||||||
"active_tab": active_tab,
|
"active_tab": active_tab,
|
||||||
"active_page": "documents",
|
"active_page": "documents",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,26 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="/documents/{{ document.document_id }}/save-document-type" style="margin-top: 1rem;">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field" style="position: relative;">
|
||||||
|
<label for="document_type_input">Document type</label>
|
||||||
|
<input
|
||||||
|
id="document_type_input"
|
||||||
|
type="text"
|
||||||
|
name="document_type"
|
||||||
|
value="{{ document.document_type or '' }}"
|
||||||
|
placeholder="receipt"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<div id="document-type-suggestions" style="display:none; position:absolute; top:100%; left:0; right:0; z-index:20; background:#fff; border:1px solid #d7dce5; border-radius:12px; margin-top:0.35rem; max-height:220px; overflow-y:auto; box-shadow:0 10px 24px rgba(15,23,42,0.10);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-row" style="margin-top: 0.75rem;">
|
||||||
|
<button type="submit">Save document type</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="queue-nav-row">
|
<div class="queue-nav-row">
|
||||||
{% if prev_doc %}
|
{% if prev_doc %}
|
||||||
<a class="button-link" href="/documents/{{ prev_doc.document_id }}">← Previous</a>
|
<a class="button-link" href="/documents/{{ prev_doc.document_id }}">← Previous</a>
|
||||||
|
|
@ -432,6 +452,59 @@
|
||||||
syncScroll();
|
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 '<button type="button" class="doc-type-option" data-value="' + item.replace(/"/g, '"') + '" style="display:block; width:100%; text-align:left; padding:0.7rem 0.85rem; border:0; background:#fff; cursor:pointer;">' + item + '</button>';
|
||||||
|
}).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";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue