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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user