from decimal import Decimal from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from sqlalchemy import create_engine, func from sqlalchemy.orm import sessionmaker from app.core.config import DATABASE_URL from app.models.document import Document from app.models.extracted_field import ExtractedField from app.models.receipt_line_item import ReceiptLineItem from app.routes.documents import router as documents_router from app.routes.health import router as health_router from app.routes.ingest import router as ingest_router from app.routes.line_items import router as line_items_router from app.routes.queue import router as queue_router from app.routes.presets import router as presets_router from app.routes.trash import router as trash_router app = FastAPI(title="document-processor") app.mount("/static", StaticFiles(directory="app/static"), name="static") app.mount("/files", StaticFiles(directory="/mnt/storage/document-processor"), name="files") app.include_router(health_router) app.include_router(documents_router) app.include_router(ingest_router) app.include_router(line_items_router) app.include_router(queue_router) app.include_router(presets_router) app.include_router(trash_router) templates = Jinja2Templates(directory="app/templates") engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(bind=engine) def _to_str(value) -> str: if value is None: return "" return str(value) @app.get("/") def root_dashboard(request: Request): db = SessionLocal() try: total_documents = db.query(func.count(Document.id)).scalar() or 0 active_documents = ( db.query(func.count(Document.id)) .filter(Document.is_trashed.is_(False)) .scalar() or 0 ) reviewed_documents = ( db.query(func.count(Document.id)) .filter(Document.is_trashed.is_(False)) .filter(Document.review_status == "reviewed") .scalar() or 0 ) pending_review_documents = ( db.query(func.count(Document.id)) .filter(Document.is_trashed.is_(False)) .filter(Document.review_status != "reviewed") .scalar() or 0 ) trashed_documents = ( db.query(func.count(Document.id)) .filter(Document.is_trashed.is_(True)) .scalar() or 0 ) extracted_documents = db.query(func.count(ExtractedField.id)).scalar() or 0 total_line_items = db.query(func.count(ReceiptLineItem.id)).scalar() or 0 cocktail_count = ( db.query(func.count(ReceiptLineItem.id)) .filter(func.lower(ReceiptLineItem.item_category) == "cocktail") .scalar() or 0 ) quality_candidates = ( db.query(ReceiptLineItem.extra_json) .filter(func.lower(ReceiptLineItem.item_category) == "cocktail") .all() ) rated_cocktails = 0 pending_cocktail_reviews = 0 na_cocktails = 0 rating_sum = Decimal("0") for (extra_json,) in quality_candidates: extra = extra_json or {} status = str(extra.get("quality_status") or "").strip().lower() rating_raw = str(extra.get("quality_rating") or "").strip() rating_dec = None if rating_raw: try: rating_dec = Decimal(rating_raw) except Exception: rating_dec = None if status == "na": na_cocktails += 1 elif rating_dec is not None: rated_cocktails += 1 rating_sum += rating_dec else: pending_cocktail_reviews += 1 avg_cocktail_rating = "" if rated_cocktails > 0: avg_cocktail_rating = str((rating_sum / Decimal(rated_cocktails)).quantize(Decimal("0.01"))) recent_documents = ( db.query(Document) .filter(Document.is_trashed.is_(False)) .order_by(Document.updated_at.desc()) .limit(10) .all() ) recent_line_items = ( db.query(ReceiptLineItem) .join(Document, ReceiptLineItem.document_id == Document.id) .filter(Document.is_trashed.is_(False)) .order_by(ReceiptLineItem.updated_at.desc()) .limit(10) .all() ) recent_line_item_rows = [] for item in recent_line_items: doc = item.document merchant = "" transaction_date = "" if doc and doc.extracted_fields: extracted = sorted( doc.extracted_fields, key=lambda x: x.updated_at or x.created_at, reverse=True, )[0] merchant = extracted.merchant_normalized or extracted.merchant_raw or "" if extracted.transaction_date: transaction_date = extracted.transaction_date.isoformat() extra = item.extra_json or {} recent_line_item_rows.append( { "document_id": doc.document_id if doc else "", "merchant": merchant, "transaction_date": transaction_date, "description": item.normalized_description or item.raw_description or "", "category": item.item_category or "", "line_total": _to_str(item.line_total), "quality_rating": _to_str(extra.get("quality_rating")), "quality_status": _to_str(extra.get("quality_status")), } ) return templates.TemplateResponse( request=request, name="dashboard.html", context={ "request": request, "active_page": "", "total_documents": total_documents, "active_documents": active_documents, "reviewed_documents": reviewed_documents, "pending_review_documents": pending_review_documents, "trashed_documents": trashed_documents, "extracted_documents": extracted_documents, "total_line_items": total_line_items, "cocktail_count": cocktail_count, "rated_cocktails": rated_cocktails, "pending_cocktail_reviews": pending_cocktail_reviews, "na_cocktails": na_cocktails, "avg_cocktail_rating": avg_cocktail_rating, "recent_documents": recent_documents, "recent_line_items": recent_line_item_rows, }, ) finally: db.close()