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:
@@ -190,8 +190,10 @@ BLOCK_PROMPTS = {
|
||||
|
||||
## כללי ציון סתירות בין שמאים (קריטי):
|
||||
- אם שני שמאים או יותר מסרו מידע שונה על אותה תכנית או היתר — חובה לסמן זאת במפורש בנוסח ניטרלי, למשל:
|
||||
> "יצוין כי השמאי X ציין כי תכנית פלונית חלה על המקרקעין במלואה, בעוד השמאי Y סבר כי חלקה של התכנית בלבד חל"
|
||||
- אין להכריע בסתירה בבלוק זה — ההכרעה (אם נדרשת) תבוא בבלוק י.
|
||||
> "יצוין כי שמאי הוועדה ציין כי תכנית פלונית חלה על המקרקעין במלואה, בעוד שמאי העורר סבר כי חלקה של התכנית בלבד חל"
|
||||
- **כשקיים שמאי מכריע** — השומה שלו היא הקובעת עובדתית. סמן זאת במפורש בסוף הדיון בסתירה, בנוסח: "ואולם, השמאי המכריע קבע כי..." או "השמאי המכריע, שבחן את עמדות הצדדים, הכריע כי...". הצג את עמדת המכריע **אחרונה** כדי שההקשר יבנה אליה.
|
||||
- השתמש בתוויות הצד המדויקות: "שמאי הוועדה המקומית", "שמאי העורר", "שמאי מכריע" — ולא בשמות פרטיים אלא אם נדרש לבהירות.
|
||||
- אין להכריע בסתירה משפטית או להגיע למסקנה נורמטיבית בבלוק זה — ההכרעה המשפטית (אם נדרשת) תבוא בבלוק י. כאן מציגים רק את הממצא העובדתי כפי שהוא, כולל הכרעת המכריע העובדתית.
|
||||
- אם אין סתירה — אין להזכיר זאת.
|
||||
|
||||
## כללים נוספים:
|
||||
@@ -502,22 +504,43 @@ async def _build_plans_context(case_id: UUID) -> str:
|
||||
return "(לא זוהו תכניות)"
|
||||
|
||||
|
||||
APPRAISER_SIDE_LABEL_HE = {
|
||||
"committee": "שמאי הוועדה המקומית",
|
||||
"appellant": "שמאי העורר",
|
||||
"deciding": "שמאי מכריע",
|
||||
"": "שמאי (לא תויג)",
|
||||
}
|
||||
|
||||
# Sort key: committee → appellant → deciding → untagged. This matches the order
|
||||
# used by db.detect_appraiser_conflicts so the deciding appraiser is last —
|
||||
# i.e. the conclusion reads most naturally ("...and the deciding appraiser ruled...").
|
||||
_SIDE_ORDER = {"committee": 1, "appellant": 2, "deciding": 3, "": 4}
|
||||
|
||||
|
||||
def _side_label(side: str) -> str:
|
||||
return APPRAISER_SIDE_LABEL_HE.get(side or "", APPRAISER_SIDE_LABEL_HE[""])
|
||||
|
||||
|
||||
async def _build_appraiser_facts_context(case_id: UUID) -> str:
|
||||
"""Group appraiser_facts by appraiser, then list each appraiser's plans+permits."""
|
||||
"""Group appraiser_facts by side (then name), list each appraiser's plans+permits."""
|
||||
facts = await db.list_appraiser_facts(case_id)
|
||||
if not facts:
|
||||
return "(לא חולצו עובדות שמאיות. הרץ extract_appraiser_facts.)"
|
||||
|
||||
by_appraiser: dict[str, dict[str, list[dict]]] = {}
|
||||
# (side, name) → {plan: [...], permit: [...]}
|
||||
groups: dict[tuple[str, str], dict[str, list[dict]]] = {}
|
||||
for f in facts:
|
||||
bucket = by_appraiser.setdefault(f["appraiser_name"], {"plan": [], "permit": []})
|
||||
key = (f.get("appraiser_side", "") or "", f["appraiser_name"])
|
||||
bucket = groups.setdefault(key, {"plan": [], "permit": []})
|
||||
bucket[f["fact_type"]].append(f)
|
||||
|
||||
ordered_keys = sorted(groups.keys(), key=lambda k: (_SIDE_ORDER.get(k[0], 9), k[1]))
|
||||
|
||||
lines: list[str] = []
|
||||
for name in sorted(by_appraiser.keys()):
|
||||
lines.append(f"\n### {name}")
|
||||
for side, name in ordered_keys:
|
||||
lines.append(f"\n### {_side_label(side)} — {name}")
|
||||
for label, key in (("תכניות", "plan"), ("היתרים", "permit")):
|
||||
items = by_appraiser[name][key]
|
||||
items = groups[(side, name)][key]
|
||||
if not items:
|
||||
continue
|
||||
lines.append(f"**{label}:**")
|
||||
@@ -543,7 +566,12 @@ async def _build_appraiser_facts_context(case_id: UUID) -> str:
|
||||
|
||||
|
||||
async def _build_appraiser_conflicts_context(case_id: UUID) -> str:
|
||||
"""Render conflict groups so the prompt can quote them in the body."""
|
||||
"""Render conflict groups so the prompt can quote them in the body.
|
||||
|
||||
Entries arrive pre-ordered from the DB by side (committee→appellant→deciding).
|
||||
When a deciding appraiser exists, the prompt must treat their view as the
|
||||
governing factual determination.
|
||||
"""
|
||||
conflicts = await db.detect_appraiser_conflicts(case_id)
|
||||
if not conflicts:
|
||||
return "(אין סתירות בין שמאים)"
|
||||
@@ -551,15 +579,19 @@ async def _build_appraiser_conflicts_context(case_id: UUID) -> str:
|
||||
type_label = {"plan": "תכנית", "permit": "היתר"}
|
||||
lines: list[str] = []
|
||||
for c in conflicts:
|
||||
lines.append(
|
||||
f"\n### סתירה — {type_label.get(c['fact_type'], c['fact_type'])}: {c['identifier']}"
|
||||
)
|
||||
has_deciding = any(e.get("appraiser_side") == "deciding" for e in c["entries"])
|
||||
header = f"\n### סתירה — {type_label.get(c['fact_type'], c['fact_type'])}: {c['identifier']}"
|
||||
if has_deciding:
|
||||
header += " _(יש שמאי מכריע — עמדתו קובעת)_"
|
||||
lines.append(header)
|
||||
for entry in c["entries"]:
|
||||
side = entry.get("appraiser_side", "") or ""
|
||||
details = entry.get("details") or {}
|
||||
scope = (details.get("scope") or "").strip()
|
||||
status = (details.get("status") or "").strip()
|
||||
quote = (details.get("raw_quote") or "").strip()
|
||||
parts = [f"**{entry['appraiser_name']}**"]
|
||||
marker = "★ " if side == "deciding" else ""
|
||||
parts = [f"**{marker}{_side_label(side)} — {entry['appraiser_name']}**"]
|
||||
if status:
|
||||
parts.append(f"סטטוס: {status}")
|
||||
if scope:
|
||||
|
||||
Reference in New Issue
Block a user