From 5cef8f9b59c4bf4b821e3b7ecbfca5c763acf135 Mon Sep 17 00:00:00 2001 From: McElwain Date: Mon, 6 Apr 2026 17:56:58 -0500 Subject: [PATCH] feat: fold quality review into queue tabs --- app/routes/queue.py | 81 +++++++++++++- app/templates/queue/index.html | 196 ++++++++++++++++++++++----------- 2 files changed, 209 insertions(+), 68 deletions(-) diff --git a/app/routes/queue.py b/app/routes/queue.py index f616f9e..c64be75 100644 --- a/app/routes/queue.py +++ b/app/routes/queue.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import Session, selectinload from app.db.deps import get_db from app.models.document import Document from app.models.extracted_field import ExtractedField +from app.models.receipt_line_item import ReceiptLineItem router = APIRouter(prefix="/queue", tags=["queue"]) @@ -16,11 +17,61 @@ BASE_DIR = Path(__file__).resolve().parent.parent templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) +def _needs_quality_review(item: ReceiptLineItem) -> bool: + if (item.item_category or "").lower() != "cocktail": + return False + + extra = item.extra_json or {} + status = str(extra.get("quality_status") or "").strip().lower() + rating = str(extra.get("quality_rating") or "").strip() + + if status == "na": + return False + if rating: + return False + + return True + + +def _quality_row(item: ReceiptLineItem) -> dict | None: + document = item.document + if document is None: + return None + + extracted = document.extracted_fields[0] if document.extracted_fields else None + + transaction_date = "" + merchant = "" + if extracted is not None: + if extracted.transaction_date: + transaction_date = extracted.transaction_date.isoformat() + merchant = extracted.merchant_normalized or extracted.merchant_raw or "" + + extra = item.extra_json or {} + + return { + "line_item_id": item.id, + "document_id": document.document_id, + "transaction_date": transaction_date, + "merchant": merchant, + "description": item.normalized_description or item.raw_description or "", + "raw_description": item.raw_description or "", + "line_total": str(item.line_total) if item.line_total is not None else "", + "category": item.item_category or "", + "quality_rating": str(extra.get("quality_rating") or ""), + "quality_note": str(extra.get("quality_note") or ""), + } + + @router.get("/", response_class=HTMLResponse) -def review_queue(request: Request, db: Session = Depends(get_db)): +def review_queue(request: Request, tab: str = "ocr", db: Session = Depends(get_db)): + if tab not in {"ocr", "fields", "quality", "recent"}: + tab = "ocr" + needs_ocr_review = ( db.query(Document) - .filter(Document.is_trashed.is_(False)).filter(Document.review_status != "reviewed") + .filter(Document.is_trashed.is_(False)) + .filter(Document.review_status != "reviewed") .order_by(Document.created_at.asc()) .all() ) @@ -28,7 +79,8 @@ def review_queue(request: Request, db: Session = Depends(get_db)): needs_field_extraction = ( db.query(Document) .options(selectinload(Document.extracted_fields)) - .filter(Document.is_trashed.is_(False)).filter(Document.review_status == "reviewed") + .filter(Document.is_trashed.is_(False)) + .filter(Document.review_status == "reviewed") .filter(~exists().where(ExtractedField.document_id == Document.id)) .order_by(Document.updated_at.asc()) .all() @@ -36,13 +88,31 @@ def review_queue(request: Request, db: Session = Depends(get_db)): recently_updated = ( db.query(Document) - .filter(Document.is_trashed.is_(False)).order_by(Document.updated_at.desc()) + .filter(Document.is_trashed.is_(False)) + .order_by(Document.updated_at.desc()) .limit(25) .all() ) + quality_candidates = ( + db.query(ReceiptLineItem) + .options( + selectinload(ReceiptLineItem.document).selectinload(Document.extracted_fields) + ) + .order_by(ReceiptLineItem.id.asc()) + .all() + ) + + needs_quality_review = [] + for item in quality_candidates: + if _needs_quality_review(item): + row = _quality_row(item) + if row is not None: + needs_quality_review.append(row) + next_ocr = needs_ocr_review[0] if needs_ocr_review else None next_fields = needs_field_extraction[0] if needs_field_extraction else None + next_quality = needs_quality_review[0] if needs_quality_review else None return templates.TemplateResponse( request=request, @@ -51,9 +121,12 @@ def review_queue(request: Request, db: Session = Depends(get_db)): "request": request, "needs_ocr_review": needs_ocr_review, "needs_field_extraction": needs_field_extraction, + "needs_quality_review": needs_quality_review, "recently_updated": recently_updated, "next_ocr": next_ocr, "next_fields": next_fields, + "next_quality": next_quality, "active_page": "queue", + "active_tab": tab, }, ) diff --git a/app/templates/queue/index.html b/app/templates/queue/index.html index 26806ce..e034765 100644 --- a/app/templates/queue/index.html +++ b/app/templates/queue/index.html @@ -13,7 +13,7 @@

Review Queue

-

Work through OCR review and field extraction in order.

+

Work through OCR review, field extraction, quality review, and recent activity.

@@ -25,75 +25,143 @@ {% if next_fields %} Next needing field extraction {% endif %} + {% if next_quality %} + Next needing quality review + {% endif %}
-

Needs OCR review ({{ needs_ocr_review|length }})

- {% if needs_ocr_review %} -
- - - - {% for doc in needs_ocr_review %} - - - - - - - {% endfor %} - -
DocumentTypeReview statusUpdated
{{ doc.document_id }}{{ doc.document_type }}{{ doc.review_status }}{{ doc.updated_at }}
-
- {% else %} -

No documents currently need OCR review.

- {% endif %} -
+
+ OCR Review + Field Extraction + Quality Review + Recently Updated +
-
-

Needs field extraction ({{ needs_field_extraction|length }})

- {% if needs_field_extraction %} -
- - - - {% for doc in needs_field_extraction %} - - - - - - - {% endfor %} - -
DocumentTypeReview statusUpdated
{{ doc.document_id }}{{ doc.document_type }}{{ doc.review_status }}{{ doc.updated_at }}
-
- {% else %} -

No reviewed documents are waiting on field extraction.

- {% endif %} -
+
+

Needs OCR review ({{ needs_ocr_review|length }})

+ {% if needs_ocr_review %} +
+ + + + + + {% for doc in needs_ocr_review %} + + + + + + + {% endfor %} + +
DocumentTypeReview statusUpdated
{{ doc.document_id }}{{ doc.document_type }}{{ doc.review_status }}{{ doc.updated_at }}
+
+ {% else %} +

No documents currently need OCR review.

+ {% endif %} +
-
-

Recently updated

- {% if recently_updated %} -
- - - - {% for doc in recently_updated %} - - - - - - - - {% endfor %} - -
DocumentTypeReview statusCurrent pathUpdated
{{ doc.document_id }}{{ doc.document_type }}{{ doc.review_status }}{{ doc.current_path }}{{ doc.updated_at }}
-
- {% endif %} +
+

Needs field extraction ({{ needs_field_extraction|length }})

+ {% if needs_field_extraction %} +
+ + + + + + {% for doc in needs_field_extraction %} + + + + + + + {% endfor %} + +
DocumentTypeReview statusUpdated
{{ doc.document_id }}{{ doc.document_type }}{{ doc.review_status }}{{ doc.updated_at }}
+
+ {% else %} +

No reviewed documents are waiting on field extraction.

+ {% endif %} +
+ +
+

Needs cocktail quality review ({{ needs_quality_review|length }})

+ {% if needs_quality_review %} + {% for row in needs_quality_review %} +
+
+
+
{{ row.transaction_date }} ยท {{ row.merchant }}
+

{{ row.description }}

+
{{ row.raw_description }}
+
+
+ {% if row.category %} + {{ row.category }} + {% endif %} + {% if row.line_total %} + ${{ row.line_total }} + {% endif %} +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + Open document +
+
+
+ {% endfor %} + {% else %} +

No cocktail line items are waiting for quality review.

+ {% endif %} +
+ +
+

Recently updated

+ {% if recently_updated %} +
+ + + + + + {% for doc in recently_updated %} + + + + + + + + {% endfor %} + +
DocumentTypeReview statusCurrent pathUpdated
{{ doc.document_id }}{{ doc.document_type }}{{ doc.review_status }}{{ doc.current_path }}{{ doc.updated_at }}
+
+ {% else %} +

No recent documents found.

+ {% endif %} +