diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 16dc144..6f332ab 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -880,12 +880,26 @@ ], "priority": "medium", "subtasks": [] + }, + { + "id": "90", + "title": "Phase 4.5 — Practice area integration", + "description": "Add practice_area + appeal_subtype to the wizard, types, schema, case header, and cases table. Gap identified after backend commit 26d09d6 (multi-tenant axis) — new Next.js UI has zero integration while vanilla UI is fully wired. Plan: ~/.claude/plans/woolly-cooking-graham.md", + "details": "", + "testStrategy": "", + "status": "in-progress", + "dependencies": [ + "86" + ], + "priority": "high", + "subtasks": [], + "updatedAt": "2026-04-11T17:13:13.931Z" } ], "metadata": { "version": "1.0.0", - "lastModified": "2026-04-11T16:25:55.570Z", - "taskCount": 58, + "lastModified": "2026-04-11T17:13:13.932Z", + "taskCount": 59, "completedCount": 53, "tags": [ "master" diff --git a/web-ui/src/components/cases/case-header.tsx b/web-ui/src/components/cases/case-header.tsx index 2a9c137..2fd4a29 100644 --- a/web-ui/src/components/cases/case-header.tsx +++ b/web-ui/src/components/cases/case-header.tsx @@ -1,6 +1,11 @@ import Link from "next/link"; import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; import { StatusBadge } from "@/components/cases/status-badge"; +import { + PRACTICE_AREA_LABELS, + APPEAL_SUBTYPE_LABELS, +} from "@/lib/practice-area"; import type { CaseDetail } from "@/lib/api/cases"; function formatDate(iso?: string | null) { @@ -30,11 +35,22 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
-
+
ערר {data?.case_number ?? "—"} {data?.status && } + {data?.practice_area && ( + + {PRACTICE_AREA_LABELS[data.practice_area]} + {data.appeal_subtype && data.appeal_subtype !== "unknown" && ( + <> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]} + )} + + )}

{data?.title ?? "טוען…"} diff --git a/web-ui/src/components/cases/cases-table.tsx b/web-ui/src/components/cases/cases-table.tsx index d372bb3..ddc4bf6 100644 --- a/web-ui/src/components/cases/cases-table.tsx +++ b/web-ui/src/components/cases/cases-table.tsx @@ -17,6 +17,7 @@ import { import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { StatusBadge } from "@/components/cases/status-badge"; +import { APPEAL_SUBTYPE_LABELS } from "@/lib/practice-area"; import type { Case } from "@/lib/api/cases"; function formatDate(iso?: string) { @@ -59,6 +60,15 @@ const columns: ColumnDef[] = [ header: "סטטוס", cell: ({ row }) => , }, + { + accessorKey: "appeal_subtype", + header: "תחום", + cell: ({ row }) => { + const s = row.original.appeal_subtype; + if (!s || s === "unknown") return ; + return {APPEAL_SUBTYPE_LABELS[s]}; + }, + }, { accessorKey: "document_count", header: "מסמכים", diff --git a/web-ui/src/components/wizard/case-wizard.tsx b/web-ui/src/components/wizard/case-wizard.tsx index 1cbee72..f4a4bd8 100644 --- a/web-ui/src/components/wizard/case-wizard.tsx +++ b/web-ui/src/components/wizard/case-wizard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -19,6 +19,10 @@ import { caseCreateSchema, expectedOutcomes, type CaseCreateInput, } from "@/lib/schemas/case"; +import { + PRACTICE_AREAS, APPEAL_SUBTYPES, deriveSubtype, + type AppealSubtype, +} from "@/lib/practice-area"; const STEPS = [ { key: "basics", label: "פרטי יסוד" }, @@ -31,7 +35,7 @@ type StepKey = (typeof STEPS)[number]["key"]; /* Fields validated at each step — lets the user fix just what's on screen * before moving forward, instead of surfacing all errors from page 1. */ const STEP_FIELDS: Record = { - basics: ["case_number", "title"], + basics: ["case_number", "title", "practice_area", "appeal_subtype"], parties: ["appellants", "respondents"], details: ["subject", "hearing_date", "expected_outcome", "notes", "property_address", "permit_number"], }; @@ -60,9 +64,28 @@ export function CaseWizard() { hearing_date: "", notes: "", expected_outcome: "", + practice_area: "appeals_committee", + appeal_subtype: "unknown", }, }); + /* + * Auto-fill appeal_subtype from the case number as the user types, but + * stop the moment they manually pick a value from the dropdown. Mirrors + * the wireSubtypeAutofill() behaviour of the vanilla UI + * (legal-ai/web/static/index.html around line 2770). + */ + const userTouchedSubtype = useRef(false); + const caseNumber = form.watch("case_number"); + const practiceArea = form.watch("practice_area"); + useEffect(() => { + if (userTouchedSubtype.current) return; + const derived = deriveSubtype(caseNumber, practiceArea); + if (derived !== form.getValues("appeal_subtype")) { + form.setValue("appeal_subtype", derived, { shouldValidate: false }); + } + }, [caseNumber, practiceArea, form]); + const stepIndex = STEPS.findIndex((s) => s.key === step); const isLast = stepIndex === STEPS.length - 1; @@ -136,6 +159,64 @@ export function CaseWizard() {

+
+
+ + ( + + )} + /> +
+
+ + ( + + )} + /> +

+ מזוהה אוטומטית ממספר התיק +

+
+
)} diff --git a/web-ui/src/lib/api/cases.ts b/web-ui/src/lib/api/cases.ts index eb73a02..d6dc20e 100644 --- a/web-ui/src/lib/api/cases.ts +++ b/web-ui/src/lib/api/cases.ts @@ -11,6 +11,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "./client"; import type { CaseCreateInput, CaseUpdateInput } from "@/lib/schemas/case"; +import type { PracticeArea, AppealSubtype } from "@/lib/practice-area"; export type CaseStatus = | "new" @@ -35,6 +36,9 @@ export type Case = { expected_outcome?: string | null; created_at?: string; updated_at?: string; + /* Multi-tenant axis — populated by backfill + server-side derive */ + practice_area?: PracticeArea; + appeal_subtype?: AppealSubtype; /* Present when loaded with detail=true */ document_count?: number; processing_count?: number; diff --git a/web-ui/src/lib/practice-area.ts b/web-ui/src/lib/practice-area.ts new file mode 100644 index 0000000..aad8800 --- /dev/null +++ b/web-ui/src/lib/practice-area.ts @@ -0,0 +1,72 @@ +/** + * Client-side mirror of mcp-server/src/legal_mcp/services/practice_area.py. + * + * Keep the enum values and derivation logic in sync with the backend — the + * server is the authority, but the UI needs the labels and derivation for + * UX (auto-fill, badges, filters). If the server adds a new practice_area + * or subtype, extend the arrays below. + * + * See also: legal-ai/docs/practice-area-separation.md + */ + +export type PracticeArea = + | "appeals_committee" + | "national_insurance" + | "labor_law"; + +export type AppealSubtype = + | "building_permit" + | "betterment_levy" + | "compensation_197" + | "unknown"; + +export const PRACTICE_AREAS: ReadonlyArray<{ + value: PracticeArea; + label: string; + enabled: boolean; +}> = [ + { value: "appeals_committee", label: "ועדת ערר", enabled: true }, + { value: "national_insurance", label: "ביטוח לאומי", enabled: false }, + { value: "labor_law", label: "דיני עבודה", enabled: false }, +]; + +export const APPEAL_SUBTYPES: ReadonlyArray<{ + value: AppealSubtype; + label: string; +}> = [ + { value: "building_permit", label: "רישוי ובנייה" }, + { value: "betterment_levy", label: "היטל השבחה" }, + { value: "compensation_197", label: "פיצויים (ס' 197)" }, + { value: "unknown", label: "לא ידוע" }, +]; + +export const PRACTICE_AREA_LABELS: Record = + Object.fromEntries(PRACTICE_AREAS.map((p) => [p.value, p.label])) as Record< + PracticeArea, + string + >; + +export const APPEAL_SUBTYPE_LABELS: Record = + Object.fromEntries(APPEAL_SUBTYPES.map((s) => [s.value, s.label])) as Record< + AppealSubtype, + string + >; + +/* + * Derive the appeal_subtype from a case number. Mirrors the Python + * `derive_subtype` in practice_area.py. The convention is the case-number + * first digit: 1xxx → building_permit, 8xxx → betterment_levy, + * 9xxx → compensation_197. Everything else, including non-appeals_committee + * domains, returns 'unknown'. + */ +export function deriveSubtype( + caseNumber: string, + practiceArea: PracticeArea = "appeals_committee", +): AppealSubtype { + if (practiceArea !== "appeals_committee") return "unknown"; + const first = caseNumber.trim().match(/^(\d)/)?.[1]; + if (first === "1") return "building_permit"; + if (first === "8") return "betterment_levy"; + if (first === "9") return "compensation_197"; + return "unknown"; +} diff --git a/web-ui/src/lib/schemas/case.ts b/web-ui/src/lib/schemas/case.ts index 7427929..e439f49 100644 --- a/web-ui/src/lib/schemas/case.ts +++ b/web-ui/src/lib/schemas/case.ts @@ -10,6 +10,7 @@ */ import { z } from "zod"; +import type { PracticeArea, AppealSubtype } from "@/lib/practice-area"; /* Appeal numbers follow the 1xxx / 8xxx / 9xxx convention from CLAUDE.md. * Two accepted formats, both hyphen-separated: @@ -60,6 +61,17 @@ export const caseCreateSchema = z.object({ expected_outcome: z.enum( expectedOutcomes.map((o) => o.value) as [string, ...string[]], ), + practice_area: z.enum([ + "appeals_committee", + "national_insurance", + "labor_law", + ] as const satisfies readonly PracticeArea[]), + appeal_subtype: z.enum([ + "building_permit", + "betterment_levy", + "compensation_197", + "unknown", + ] as const satisfies readonly AppealSubtype[]), }); export type CaseCreateInput = z.infer;