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

@@ -491,8 +491,17 @@ CREATE TABLE IF NOT EXISTS appraiser_facts (
CREATE INDEX IF NOT EXISTS idx_appraiser_facts_case ON appraiser_facts(case_id, fact_type);
CREATE INDEX IF NOT EXISTS idx_appraiser_facts_identifier ON appraiser_facts(case_id, identifier);
-- V5.1: appraiser_side — which party this appraiser represents.
-- Values: 'committee' (הוועדה), 'appellant' (העורר), 'deciding' (מכריע).
-- Required by extract_appraiser_facts; the chair tags it via the UI before extraction.
-- Set via documents.metadata.appraiser_side at upload/edit time, then propagated here
-- so that conflict rendering in block-tet can label each entry with its side.
ALTER TABLE appraiser_facts ADD COLUMN IF NOT EXISTS appraiser_side TEXT DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_appraiser_facts_side ON appraiser_facts(case_id, appraiser_side);
-- documents.metadata.is_post_hearing: flag for materials submitted after the hearing
-- (השלמות טיעון, הצעות פשרה). Used by block-chet to include them in the proceedings narrative.
-- documents.metadata.appraiser_side: which side the appraiser represents (see above).
-- No schema change needed — uses existing JSONB metadata column.
"""
@@ -1333,7 +1342,7 @@ async def replace_appraiser_facts(
) -> int:
"""Replace all appraiser_facts for a given document.
Each fact dict: appraiser_name, fact_type ('plan'|'permit'),
Each fact dict: appraiser_name, appraiser_side, fact_type ('plan'|'permit'),
identifier, details (dict), page_number (optional).
"""
pool = await get_pool()
@@ -1345,11 +1354,12 @@ async def replace_appraiser_facts(
for f in facts:
await conn.execute(
"""INSERT INTO appraiser_facts
(case_id, document_id, appraiser_name, fact_type,
identifier, details, page_number)
VALUES ($1, $2, $3, $4, $5, $6, $7)""",
(case_id, document_id, appraiser_name, appraiser_side,
fact_type, identifier, details, page_number)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)""",
case_id, document_id,
f["appraiser_name"],
f.get("appraiser_side", ""),
f["fact_type"],
f["identifier"],
json.dumps(f.get("details", {}), ensure_ascii=False),
@@ -1396,7 +1406,9 @@ async def detect_appraiser_conflicts(case_id: UUID) -> list[dict]:
A conflict exists when the SAME identifier (e.g., "תמ"א 38") was reported
differently by two appraisers — different details, or one cited it and the
other did not. Returns list of conflict groups.
other did not. Returns list of conflict groups. Each entry in a group
carries the appraiser's side so the caller can label it as committee /
appellant / deciding.
"""
pool = await get_pool()
async with pool.acquire() as conn:
@@ -1404,10 +1416,19 @@ async def detect_appraiser_conflicts(case_id: UUID) -> list[dict]:
"""SELECT identifier, fact_type,
json_agg(jsonb_build_object(
'appraiser_name', appraiser_name,
'appraiser_side', appraiser_side,
'details', details,
'page_number', page_number,
'document_id', document_id
) ORDER BY appraiser_name) AS entries,
) ORDER BY
CASE appraiser_side
WHEN 'committee' THEN 1
WHEN 'appellant' THEN 2
WHEN 'deciding' THEN 3
ELSE 4
END,
appraiser_name
) AS entries,
COUNT(DISTINCT appraiser_name) AS n_appraisers
FROM appraiser_facts
WHERE case_id = $1