Phase 4.5: practice area integration in the Next.js UI

Backend commit 26d09d6 introduced a multi-tenant axis (practice_area +
appeal_subtype) that the vanilla UI picked up but the new Next.js
rewrite did not. Close the gap in the screens we already shipped so
future search/filter work in Phase 5 has the right data on screen.

- lib/practice-area.ts — new: enum + label maps + deriveSubtype(),
  mirrors mcp-server/src/legal_mcp/services/practice_area.py.
- lib/schemas/case.ts — two new z.enum fields on caseCreateSchema.
- lib/api/cases.ts — Case / CaseDetail gain practice_area and
  appeal_subtype as optional (cached pre-migration responses still
  parse cleanly).
- wizard/case-wizard.tsx — basics step gains a practice_area dropdown
  (future domains disabled with "(בקרוב)") and an appeal_subtype
  dropdown with auto-fill effect tracking a userTouchedSubtype ref,
  same behaviour as wireSubtypeAutofill() in the vanilla UI.
- cases/case-header.tsx — gold badge next to the status shows
  "ועדת ערר · רישוי ובנייה" when both fields are populated.
- cases/cases-table.tsx — new "תחום" column showing subtype label
  (dash for unknown). No filter yet — that's phase 5 when a second
  domain actually exists.

Plan: ~/.claude/plans/woolly-cooking-graham.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 17:15:48 +00:00
parent e483eba1a9
commit ac0a5ee30b
7 changed files with 214 additions and 5 deletions

View File

@@ -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<StepKey, (keyof CaseCreateInput)[]> = {
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() {
<Input id="title" {...form.register("title")} className="mt-1" />
<FieldError message={form.formState.errors.title?.message} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-navy">תחום משפטי</Label>
<Controller
control={form.control}
name="practice_area"
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange} dir="rtl">
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PRACTICE_AREAS.map((p) => (
<SelectItem
key={p.value}
value={p.value}
disabled={!p.enabled}
>
{p.label}{!p.enabled && " (בקרוב)"}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</div>
<div>
<Label className="text-navy">סוג ערר</Label>
<Controller
control={form.control}
name="appeal_subtype"
render={({ field }) => (
<Select
value={field.value}
onValueChange={(v) => {
userTouchedSubtype.current = true;
field.onChange(v as AppealSubtype);
}}
dir="rtl"
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{APPEAL_SUBTYPES.map((s) => (
<SelectItem key={s.value} value={s.value}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<p className="text-[0.7rem] text-ink-muted mt-1">
מזוהה אוטומטית ממספר התיק
</p>
</div>
</div>
</div>
)}