Edit document doc_type and appraiser side from the case UI
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m26s
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:
@@ -392,3 +392,98 @@ async def get_claims(case_number: str, party_role: str = "") -> str:
|
||||
})
|
||||
|
||||
return json.dumps(formatted, default=str, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# Whitelist of doc_type values; mirrors web/app.py:DOC_TYPE_NAMES.
|
||||
ALLOWED_DOC_TYPES = {
|
||||
"appeal", "response", "protocol", "plan", "decision",
|
||||
"court_decision", "permit", "appraisal", "exhibit",
|
||||
"objection", "reference",
|
||||
}
|
||||
|
||||
# Allowed appraiser_side values; '' (empty) clears the tag.
|
||||
ALLOWED_APPRAISER_SIDES = {"committee", "appellant", "deciding", ""}
|
||||
|
||||
|
||||
async def document_update(
|
||||
case_number: str,
|
||||
doc_id: str,
|
||||
doc_type: str = "",
|
||||
appraiser_side: str = "",
|
||||
) -> str:
|
||||
"""עדכון תיוג מסמך — doc_type ו/או appraiser_side. ריק = אין שינוי.
|
||||
|
||||
הולידציה זהה ל-PATCH endpoint ב-web/app.py. appraiser_side נשמר ב-
|
||||
documents.metadata JSONB (מתפרסם משם ע"י extract_appraiser_facts).
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר (לאישור שייכות)
|
||||
doc_id: UUID של המסמך
|
||||
doc_type: ערך חדש (appeal/response/protocol/plan/decision/court_decision/
|
||||
permit/appraisal/exhibit/objection/reference). ריק = אין שינוי.
|
||||
appraiser_side: ערך חדש (committee/appellant/deciding). ריק = אין שינוי;
|
||||
העבר במפורש מחרוזת ריקה לא-default אם רוצים לנקות.
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"תיק {case_number} לא נמצא."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
try:
|
||||
doc_uuid = UUID(doc_id)
|
||||
except ValueError:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"doc_id לא תקין: {doc_id}"},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
doc = await db.get_document(doc_uuid)
|
||||
if not doc:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"מסמך {doc_id} לא נמצא."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
if doc.get("case_id") != case["id"]:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"מסמך {doc_id} לא שייך לתיק {case_number}."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
updates: dict = {}
|
||||
|
||||
if doc_type:
|
||||
if doc_type not in ALLOWED_DOC_TYPES:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"message": f"doc_type לא תקין: {doc_type}",
|
||||
"allowed": sorted(ALLOWED_DOC_TYPES),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
updates["doc_type"] = doc_type
|
||||
|
||||
# appraiser_side is optional. The MCP tool can't distinguish "skip" from
|
||||
# "set to empty string", so we use the convention: only update if non-empty.
|
||||
# To clear, the operator must edit metadata directly (rare).
|
||||
if appraiser_side:
|
||||
if appraiser_side not in ALLOWED_APPRAISER_SIDES:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"message": f"appraiser_side לא תקין: {appraiser_side}",
|
||||
"allowed": sorted(s for s in ALLOWED_APPRAISER_SIDES if s),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
metadata = doc.get("metadata") or {}
|
||||
if isinstance(metadata, str):
|
||||
metadata = json.loads(metadata)
|
||||
metadata["appraiser_side"] = appraiser_side
|
||||
updates["metadata"] = metadata
|
||||
|
||||
if not updates:
|
||||
return json.dumps({"status": "noop", "message": "אין שינוי לבצע."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
await db.update_document(doc_uuid, **updates)
|
||||
fresh = await db.get_document(doc_uuid)
|
||||
return json.dumps({
|
||||
"status": "completed",
|
||||
"doc_id": doc_id,
|
||||
"doc_type": fresh.get("doc_type"),
|
||||
"metadata": fresh.get("metadata"),
|
||||
}, default=str, ensure_ascii=False, indent=2)
|
||||
|
||||
Reference in New Issue
Block a user