diff --git a/mcp-server/src/legal_mcp/server.py b/mcp-server/src/legal_mcp/server.py index bfea3a3..313903e 100644 --- a/mcp-server/src/legal_mcp/server.py +++ b/mcp-server/src/legal_mcp/server.py @@ -187,6 +187,17 @@ async def document_list(case_number: str) -> str: return await documents.document_list(case_number) +@mcp.tool() +async def document_update( + case_number: str, + doc_id: str, + doc_type: str = "", + appraiser_side: str = "", +) -> str: + """עדכון תיוג מסמך — doc_type ו/או appraiser_side (committee/appellant/deciding). ריק = ללא שינוי.""" + return await documents.document_update(case_number, doc_id, doc_type, appraiser_side) + + # Claims extraction @mcp.tool() async def extract_claims( diff --git a/mcp-server/src/legal_mcp/services/appraiser_facts_extractor.py b/mcp-server/src/legal_mcp/services/appraiser_facts_extractor.py index 68a43a9..4b6f63c 100644 --- a/mcp-server/src/legal_mcp/services/appraiser_facts_extractor.py +++ b/mcp-server/src/legal_mcp/services/appraiser_facts_extractor.py @@ -4,11 +4,16 @@ לאפשר זיהוי אוטומטי של סתירות בין שמאים שונים על אותו זיהוי (תכנית או היתר). שמירה ב-DB: טבלת appraiser_facts (case_id, document_id, appraiser_name, -fact_type, identifier, details JSONB, page_number). +appraiser_side, fact_type, identifier, details JSONB, page_number). + +Precondition: כל מסמך doc_type='appraisal' חייב להיות מתויג עם +metadata.appraiser_side מתוך {committee, appellant, deciding}. החילוץ עוצר +ומחזיר status='sides_missing' אם יש מסמכים לא מתויגים. """ from __future__ import annotations +import json import logging from uuid import UUID @@ -17,6 +22,13 @@ from legal_mcp.services import claude_session, db logger = logging.getLogger(__name__) +# Allowed sides for an appraiser in an appeals committee case. +# committee = שמאי הוועדה המקומית +# appellant = שמאי העורר / הצד שכנגד הוועדה +# deciding = שמאי מכריע +VALID_APPRAISER_SIDES = {"committee", "appellant", "deciding"} + + EXTRACT_FACTS_PROMPT = """אתה מנתח שומות מקרקעין לטובת ועדת ערר לתכנון ובניה. תפקידך: לחלץ מתוך השומה שתי קטגוריות של עובדות אובייקטיביות שעליהן השמאי מבסס את חוות דעתו: @@ -77,6 +89,7 @@ async def extract_facts_from_document( case_id: UUID, document_id: UUID, appraiser_name: str, + appraiser_side: str, text: str, ) -> list[dict]: """Extract structured facts from a single appraisal document via Claude Code.""" @@ -107,32 +120,63 @@ async def extract_facts_from_document( continue all_facts.append({ "appraiser_name": appraiser_name, + "appraiser_side": appraiser_side, "fact_type": item["fact_type"], "identifier": _normalize_identifier(ident), "details": item.get("details") or {}, "page_number": item.get("page_number"), }) - if all_facts: - await db.replace_appraiser_facts(case_id, document_id, all_facts) - else: - await db.replace_appraiser_facts(case_id, document_id, []) + await db.replace_appraiser_facts(case_id, document_id, all_facts) return all_facts +def _doc_metadata(doc: dict) -> dict: + metadata = doc.get("metadata") or {} + if isinstance(metadata, str): + try: + metadata = json.loads(metadata) + except json.JSONDecodeError: + metadata = {} + return metadata if isinstance(metadata, dict) else {} + + def _infer_appraiser_name(doc: dict) -> str: """Best-effort extraction of the appraiser's name from document title/metadata.""" - metadata = doc.get("metadata") or {} - name = metadata.get("appraiser_name") if isinstance(metadata, dict) else None + meta = _doc_metadata(doc) + name = meta.get("appraiser_name") if name: return name title = doc.get("title", "") return title or f"שמאי (מסמך {doc.get('id', '')[:8]})" +def _get_appraiser_side(doc: dict) -> str: + """Return the tagged side, or '' if not tagged.""" + return _doc_metadata(doc).get("appraiser_side", "") or "" + + +def _validate_sides_tagged(appraisals: list[dict]) -> list[dict]: + """Return the subset of appraisals missing a valid appraiser_side tag.""" + missing: list[dict] = [] + for doc in appraisals: + side = _get_appraiser_side(doc) + if side not in VALID_APPRAISER_SIDES: + missing.append({ + "document_id": doc["id"], + "title": doc.get("title", ""), + "current_side": side, + }) + return missing + + async def extract_appraiser_facts(case_id: UUID) -> dict: """Extract facts from every appraisal document in the case + detect conflicts. + Blocks if any appraisal is missing metadata.appraiser_side — the chair must + tag each one via the UI before extraction runs, so that conflict rendering + in block-tet can identify the deciding appraiser's view as authoritative. + Returns a summary dict ready for serialization back to the caller. """ docs = await db.list_documents(case_id) @@ -146,6 +190,18 @@ async def extract_appraiser_facts(case_id: UUID) -> dict: "conflicts": [], } + missing_sides = _validate_sides_tagged(appraisals) + if missing_sides: + return { + "status": "sides_missing", + "appraisal_count": len(appraisals), + "missing": missing_sides, + "message": ( + "חסר תיוג appraiser_side במסמכי שומה. תייג כל שומה דרך ה-UI " + "(ועדה / עורר / מכריע) והרץ שוב." + ), + } + by_doc = [] total_facts = 0 for doc in appraisals: @@ -160,11 +216,13 @@ async def extract_appraiser_facts(case_id: UUID) -> dict: continue appraiser_name = _infer_appraiser_name(doc) + appraiser_side = _get_appraiser_side(doc) try: facts = await extract_facts_from_document( case_id=case_id, document_id=UUID(doc["id"]), appraiser_name=appraiser_name, + appraiser_side=appraiser_side, text=text, ) except Exception as e: @@ -183,6 +241,7 @@ async def extract_appraiser_facts(case_id: UUID) -> dict: "document_id": doc["id"], "title": doc.get("title", ""), "appraiser_name": appraiser_name, + "appraiser_side": appraiser_side, "status": "completed", "facts_extracted": len(facts), "plans": sum(1 for f in facts if f["fact_type"] == "plan"), diff --git a/mcp-server/src/legal_mcp/services/block_writer.py b/mcp-server/src/legal_mcp/services/block_writer.py index e1549ff..4100dc2 100644 --- a/mcp-server/src/legal_mcp/services/block_writer.py +++ b/mcp-server/src/legal_mcp/services/block_writer.py @@ -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: diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index ccbf365..f817789 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -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 diff --git a/mcp-server/src/legal_mcp/tools/documents.py b/mcp-server/src/legal_mcp/tools/documents.py index d984a80..8faad6b 100644 --- a/mcp-server/src/legal_mcp/tools/documents.py +++ b/mcp-server/src/legal_mcp/tools/documents.py @@ -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) diff --git a/web-ui/src/components/cases/document-type-editor.tsx b/web-ui/src/components/cases/document-type-editor.tsx new file mode 100644 index 0000000..20c5b04 --- /dev/null +++ b/web-ui/src/components/cases/document-type-editor.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { useState } from "react"; +import { Loader2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + APPRAISER_SIDE_LABELS, + APPRAISER_SIDE_OPTIONS, + DOC_TYPE_OPTIONS, + appraiserSideLabel, + doctypeLabel, + doctypeTone, + type AppraiserSide, + type DocType, +} from "@/lib/doc-types"; +import { usePatchDocument } from "@/lib/api/documents"; + +/* + * Inline editor for a document's tags. Renders a colored Badge that opens a + * Popover with two Selects: + * 1. doc_type (always shown) + * 2. appraiser_side (only when doc_type === "appraisal") + * + * The Save button is disabled when doc_type is "appraisal" but no side is + * picked — extract_appraiser_facts requires it, so we enforce here too. + */ + +export function DocumentTypeEditor({ + caseNumber, + docId, + docType, + appraiserSide, +}: { + caseNumber: string; + docId: string; + docType: string; + appraiserSide?: string; +}) { + const [open, setOpen] = useState(false); + const [draftType, setDraftType] = useState(docType || ""); + const [draftSide, setDraftSide] = useState(appraiserSide || ""); + const patch = usePatchDocument(caseNumber); + + function reset() { + setDraftType(docType || ""); + setDraftSide(appraiserSide || ""); + } + + const isAppraisal = draftType === "appraisal"; + const sideMissing = isAppraisal && !draftSide; + const dirty = + draftType !== docType || + (isAppraisal && draftSide !== (appraiserSide || "")); + + async function handleSave() { + if (sideMissing || !dirty) return; + const body: { doc_type?: string; appraiser_side?: string } = {}; + if (draftType !== docType) body.doc_type = draftType; + if (isAppraisal && draftSide !== (appraiserSide || "")) { + body.appraiser_side = draftSide; + } + // If the new type is NOT appraisal but the doc previously had a side, + // clear it so it doesn't dangle confusingly in metadata. + if (!isAppraisal && appraiserSide) body.appraiser_side = ""; + + await patch.mutateAsync({ docId, patch: body }); + setOpen(false); + } + + // Build the on-badge label: "שומה · שמאי הוועדה" when both present. + const badgeText = + docType === "appraisal" && appraiserSide + ? `${doctypeLabel(docType)} · ${appraiserSideLabel(appraiserSide)}` + : doctypeLabel(docType); + + return ( + { + setOpen(next); + if (next) reset(); + }} + > + + + + +
+ + +
+ + {isAppraisal && ( +
+ + + {sideMissing && ( +

+ נדרש לציין את הצד לפני חילוץ עובדות שמאיות. +

+ )} +

+ ערכים: {Object.values(APPRAISER_SIDE_LABELS).join(" · ")} +

+
+ )} + + {patch.isError && ( +

+ שמירה נכשלה. נסה שוב. +

+ )} + +
+ + +
+
+
+ ); +} + +// Re-export types so callers don't need to dual-import. +export type { AppraiserSide, DocType }; diff --git a/web-ui/src/components/cases/documents-panel.tsx b/web-ui/src/components/cases/documents-panel.tsx index 825d3ee..19a25ec 100644 --- a/web-ui/src/components/cases/documents-panel.tsx +++ b/web-ui/src/components/cases/documents-panel.tsx @@ -1,7 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { @@ -23,40 +22,19 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@/lib/api/client"; import { casesKeys } from "@/lib/api/cases"; import type { CaseDetail, CaseDocument } from "@/lib/api/cases"; +import { DocumentTypeEditor } from "@/components/cases/document-type-editor"; /* * Document list for the case detail "מסמכים" tab. Uses the real document * row shape returned by the FastAPI case_get endpoint — see db.list_documents * and the `documents` schema in legal_mcp/services/db.py: * id · case_id · doc_type · title · file_path · extraction_status · - * page_count · created_at · practice_area · appeal_subtype + * page_count · created_at · practice_area · appeal_subtype · metadata + * + * Doc-type labels and tone classes live in @/lib/doc-types so the upload + * sheet, the inline editor, and this panel all stay in sync. */ -const DOC_TYPE_LABELS: Record = { - appeal: "כתב ערר", - response: "כתב תשובה", - protocol: "פרוטוקול", - decision: "החלטת ועדה מקומית", - plan: "תכנית", - appraisal: "שומה", - reference: "חומר רקע", - auto: "—", -}; - -function doctypeLabel(t: string): string { - return DOC_TYPE_LABELS[t] ?? t; -} - -function doctypeTone(t: string): string { - switch (t) { - case "appeal": return "bg-info-bg text-info border-info/40"; - case "response": return "bg-gold-wash text-gold-deep border-gold/40"; - case "decision": return "bg-success-bg text-success border-success/40"; - case "protocol": return "bg-warn-bg text-warn border-warn/40"; - default: return "bg-rule-soft text-ink-muted border-rule"; - } -} - const STATUS_LABELS: Record = { pending: "בהמתנה", processing: "בעיבוד", @@ -272,12 +250,15 @@ function DocumentRow({ {doc.doc_type && ( - - {doctypeLabel(doc.doc_type)} - + )}