import json import asyncio from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.templating import Jinja2Templates from config import UPLOAD_DIR from database.connection import get_db from services.extractor import extract_document from services.search_service import normalize_arabic router = APIRouter() templates = Jinja2Templates(directory="templates") def _get_document(doc_id: int) -> dict | None: with get_db() as conn: row = conn.execute("SELECT * FROM documents WHERE id=?", (doc_id,)).fetchone() if not row: return None doc = dict(row) props = conn.execute( "SELECT * FROM properties WHERE document_id=? ORDER BY row_order", (doc_id,), ).fetchall() doc["properties"] = [dict(p) for p in props] if doc["person_id"]: person = conn.execute( "SELECT * FROM persons WHERE id=?", (doc["person_id"],) ).fetchone() doc["person"] = dict(person) if person else {} elif doc["raw_extraction_json"]: try: extracted = json.loads(doc["raw_extraction_json"]) doc["person"] = extracted.get("person", {}) except Exception: doc["person"] = {} else: doc["person"] = {} return doc @router.get("/review/next") async def review_next(): with get_db() as conn: row = conn.execute( "SELECT id FROM documents WHERE status='extracted' ORDER BY id LIMIT 1" ).fetchone() if row: return RedirectResponse(f"/review/{row['id']}", status_code=302) with get_db() as conn: pending = conn.execute( "SELECT COUNT(*) AS n FROM documents WHERE status='pending'" ).fetchone()["n"] if pending: return HTMLResponse( "" "

جارٍ معالجة الوثائق...

" "

أعد التحميل بعد قليل أو اذهب إلى قائمة الوثائق.

" "" ) return HTMLResponse( "" "

تمت مراجعة جميع الوثائق!

" "

البحث في قاعدة البيانات | رفع المزيد

" "" ) @router.get("/review/{doc_id}") async def review_document(request: Request, doc_id: int, wait: int = 0): doc = _get_document(doc_id) if not doc: return HTMLResponse("Document not found", status_code=404) if doc["status"] == "pending" and wait: return HTMLResponse( f""" Processing...

جارٍ قراءة الوثيقة...

ستتحدث هذه الصفحة تلقائياً.

تحديث الآن

""" ) return templates.TemplateResponse( request, "review.html", {"doc": doc, "upload_dir": "/uploads"} ) @router.get("/api/check-duplicate") async def check_duplicate( first_name: str = "", father_name: str = "", family_name: str = "", ): """Check if a person with similar name already exists. Called via AJAX from review page.""" if not first_name: return JSONResponse({"matches": []}) first_norm = normalize_arabic(first_name.strip()) family_norm = normalize_arabic(family_name.strip()) if family_name else "" with get_db() as conn: if family_norm: rows = conn.execute( """SELECT p.*, COUNT(DISTINCT pr.id) AS property_count FROM persons p LEFT JOIN properties pr ON pr.person_id = p.id WHERE p.first_name_norm = ? AND p.family_name_norm = ? GROUP BY p.id""", (first_norm, family_norm), ).fetchall() else: rows = conn.execute( """SELECT p.*, COUNT(DISTINCT pr.id) AS property_count FROM persons p LEFT JOIN properties pr ON pr.person_id = p.id WHERE p.first_name_norm = ? GROUP BY p.id""", (first_norm,), ).fetchall() # Further filter by father_name if provided matches = [] for r in rows: d = dict(r) if father_name and d.get("father_name"): if normalize_arabic(father_name.strip()) != normalize_arabic(d["father_name"]): continue matches.append({ "id": d["id"], "first_name": d["first_name"], "father_name": d.get("father_name"), "family_name": d.get("family_name"), "family_origin": d.get("family_origin"), "property_count": d["property_count"], }) return JSONResponse({"matches": matches}) @router.post("/confirm/{doc_id}") async def confirm_document(doc_id: int, request: Request): body = await request.json() person_data = body.get("person", {}) properties_data = body.get("properties", []) merge_person_id = body.get("merge_person_id") # If user chose to merge first_name = (person_data.get("first_name") or "").strip() registry_number = (person_data.get("registry_number") or "").strip() or None # We should update document fields as well since user might have edited them request_number = (body.get("request_number") or "").strip() or None request_date = (body.get("request_date") or "").strip() or None page_info = (body.get("page_info") or "").strip() or None search_scope = (body.get("search_scope") or "").strip() or None request_purpose = (body.get("request_purpose") or "").strip() or None data_valid_until = (body.get("data_valid_until") or "").strip() or None registry_office = (body.get("registry_office") or "").strip() or None owns_properties = body.get("owns_properties") if owns_properties is not None: owns_properties = bool(owns_properties) declared_property_count = body.get("declared_property_count") if declared_property_count is not None: try: declared_property_count = int(declared_property_count) except ValueError: declared_property_count = None with get_db() as conn: person_id = None # Option 1: User explicitly chose to merge with an existing person if merge_person_id: person_id = int(merge_person_id) # Update person info with latest data conn.execute( """UPDATE persons SET first_name=?, father_name=?, mother_name=?, family_name=?, family_origin=?, nationality=?, birth_date=?, registry_place=?, registry_number=COALESCE(registry_number, ?), first_name_norm=?, family_name_norm=?, updated_at=CURRENT_TIMESTAMP WHERE id=?""", ( first_name, person_data.get("father_name"), person_data.get("mother_name"), person_data.get("family_name"), person_data.get("family_origin"), person_data.get("nationality"), person_data.get("birth_date"), person_data.get("registry_place"), registry_number, normalize_arabic(first_name), normalize_arabic(person_data.get("family_name") or ""), person_id, ), ) # Option 2: Auto-merge by registry_number if not person_id and registry_number: existing = conn.execute( "SELECT id FROM persons WHERE registry_number=?", (registry_number,) ).fetchone() if existing: person_id = existing["id"] conn.execute( """UPDATE persons SET first_name=?, father_name=?, mother_name=?, family_name=?, family_origin=?, nationality=?, birth_date=?, registry_place=?, first_name_norm=?, family_name_norm=?, updated_at=CURRENT_TIMESTAMP WHERE id=?""", ( first_name, person_data.get("father_name"), person_data.get("mother_name"), person_data.get("family_name"), person_data.get("family_origin"), person_data.get("nationality"), person_data.get("birth_date"), person_data.get("registry_place"), normalize_arabic(first_name), normalize_arabic(person_data.get("family_name") or ""), person_id, ), ) # Option 3: Create new person if not person_id: cursor = conn.execute( """INSERT INTO persons (first_name, father_name, mother_name, family_name, family_origin, nationality, birth_date, registry_number, registry_place, first_name_norm, family_name_norm) VALUES (?,?,?,?,?,?,?,?,?,?,?)""", ( first_name, person_data.get("father_name"), person_data.get("mother_name"), person_data.get("family_name"), person_data.get("family_origin"), person_data.get("nationality"), person_data.get("birth_date"), registry_number, person_data.get("registry_place"), normalize_arabic(first_name), normalize_arabic(person_data.get("family_name") or ""), ), ) person_id = cursor.lastrowid # Replace properties for this document conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,)) for i, prop in enumerate(properties_data): conn.execute( """INSERT INTO properties (document_id, person_id, row_order, party_name, property_number, section, block, real_estate_district, qaza, num_shares, ownership_type) VALUES (?,?,?,?,?,?,?,?,?,?,?)""", ( doc_id, person_id, i, prop.get("party_name"), prop.get("property_number"), prop.get("section"), prop.get("block"), prop.get("real_estate_district"), prop.get("qaza"), prop.get("num_shares"), prop.get("ownership_type"), ), ) conn.execute( """UPDATE documents SET status='confirmed', person_id=?, request_number=?, request_date=?, page_info=?, search_scope=?, request_purpose=?, data_valid_until=?, registry_office=?, owns_properties=?, declared_property_count=?, updated_at=CURRENT_TIMESTAMP WHERE id=?""", (person_id, request_number, request_date, page_info, search_scope, request_purpose, data_valid_until, registry_office, owns_properties, declared_property_count, doc_id), ) return JSONResponse({"ok": True, "person_id": person_id, "next": "/review/next"}) @router.post("/extract/{doc_id}") async def retrigger_extraction(doc_id: int, provider: str = ""): """Re-run extraction for a document (retry after error).""" with get_db() as conn: doc = conn.execute( "SELECT image_path, provider FROM documents WHERE id=?", (doc_id,) ).fetchone() if not doc: return JSONResponse({"error": "not found"}, status_code=404) with get_db() as conn: conn.execute( "UPDATE documents SET status='pending', extraction_error=NULL WHERE id=?", (doc_id,), ) conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,)) from routers.upload import _extract_and_save use_provider = provider or doc["provider"] or "" asyncio.create_task(_extract_and_save(doc_id, doc["image_path"], use_provider)) return JSONResponse({"ok": True, "message": "Extraction started"})