Edit document doc_type and appraiser side from the case UI
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m26s

Until now changing a document's doc_type required a manual SQL update.
Adds an inline editor on the document badge so the chair can retag
without leaving the case page, and threads an appraiser_side tag
(committee / appellant / deciding) through the appraisal pipeline so
betterment-levy cases — which usually have 2-3 appraisers — render
conflicts with the deciding appraiser's view marked as governing.

Backend
- New appraiser_facts.appraiser_side column (V5.1) populated from
  documents.metadata.appraiser_side at extraction time.
- extract_appraiser_facts now returns status='sides_missing' with the
  list of untagged appraisals instead of running with empty side
  labels — chair must tag every appraisal first via the UI.
- Conflict detection orders entries committee → appellant → deciding so
  the deciding appraiser appears last; block-tet's prompt instructs the
  writer to phrase the deciding appraiser's view as the governing
  factual finding ("ואולם, השמאי המכריע קבע...").
- New PATCH /api/cases/{n}/documents/{doc_id} (Pydantic model with
  whitelist validation) and matching document_update MCP tool. Both
  merge appraiser_side into metadata JSONB instead of touching the
  schema.

UI
- New shared doc-types module exports the canonical 11 doc_type
  options plus the 3 appraiser-side options; both upload-sheet and
  the document badge now read from it instead of duplicating Hebrew
  labels.
- New DocumentTypeEditor renders a Popover off the doc-type Badge
  with two Selects. The save button stays disabled while doc_type is
  appraisal but no side has been picked, mirroring the backend
  enforcement so the user finds out before triggering extraction.
- usePatchDocument React-Query mutation invalidates the case detail
  on success so the badge updates without a manual refresh.
This commit is contained in:
2026-04-19 06:26:51 +00:00
parent 110901a66c
commit c536ed0e63
12 changed files with 655 additions and 67 deletions

View File

@@ -2999,6 +2999,71 @@ async def api_reprocess_document(case_number: str, doc_id: str):
return {"status": "reprocessing"}
_ALLOWED_APPRAISER_SIDES = {"committee", "appellant", "deciding"}
class DocumentPatchRequest(BaseModel):
"""Patch payload for a single document. Both fields are optional."""
doc_type: str | None = None
appraiser_side: str | None = None # committee | appellant | deciding | "" to clear
@app.patch("/api/cases/{case_number}/documents/{doc_id}")
async def api_patch_document(case_number: str, doc_id: str, req: DocumentPatchRequest):
"""Update a document's tags. Currently supports doc_type and the
metadata.appraiser_side flag (used by extract_appraiser_facts).
Returns the refreshed document row.
"""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
try:
document_id = UUID(doc_id)
except ValueError:
raise HTTPException(400, f"doc_id לא תקין: {doc_id}")
doc = await db.get_document(document_id)
if not doc or UUID(doc["case_id"]) != UUID(case["id"]):
raise HTTPException(404, "מסמך לא נמצא בתיק")
updates: dict = {}
if req.doc_type is not None:
if req.doc_type not in DOC_TYPE_NAMES:
raise HTTPException(
422,
f"doc_type לא תקין: {req.doc_type}. ערכים מותרים: "
f"{', '.join(sorted(DOC_TYPE_NAMES.keys()))}",
)
updates["doc_type"] = req.doc_type
if req.appraiser_side is not None:
if req.appraiser_side and req.appraiser_side not in _ALLOWED_APPRAISER_SIDES:
raise HTTPException(
422,
f"appraiser_side לא תקין: {req.appraiser_side}. ערכים מותרים: "
f"{', '.join(sorted(_ALLOWED_APPRAISER_SIDES))}",
)
metadata = doc.get("metadata") or {}
if isinstance(metadata, str):
metadata = json.loads(metadata)
if req.appraiser_side:
metadata["appraiser_side"] = req.appraiser_side
else:
metadata.pop("appraiser_side", None)
updates["metadata"] = metadata
if not updates:
return {"status": "noop", "document": doc}
await db.update_document(document_id, **updates)
fresh = await db.get_document(document_id)
return {"status": "completed", "document": fresh}
@app.delete("/api/cases/{case_number}/documents/{doc_id}")
async def api_delete_document(case_number: str, doc_id: str):
"""Delete a single document from a case (including its chunks and file)."""