Phase 4b: Case create wizard
New /cases/new route with a 3-step wizard (basics / parties / details) backed by react-hook-form + the caseCreateSchema. Each step validates only its own fields so the user fixes errors in context. On success useCreateCase invalidates the case list and the router pushes to the freshly created case detail page. PartiesField is a small chip-style editor for the appellants/respondents arrays. The Home page now has a navy "+ תיק חדש" button that links to the wizard. Dropped .default() from the create schema — zod's input/output type mismatch broke the RHF zodResolver generics; dropping the defaults is simpler than plumbing z.input vs z.output through the mutation hook. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
web-ui/src/components/wizard/parties-field.tsx
Normal file
89
web-ui/src/components/wizard/parties-field.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { X, Plus } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
/*
|
||||
* Minimal tag-style editor for a list of party names (appellants / respondents).
|
||||
* Backed by a controlled string[] — submits as the same shape the FastAPI
|
||||
* CaseCreateRequest expects. Enter adds the current draft; X removes a chip.
|
||||
*/
|
||||
|
||||
export function PartiesField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
}: {
|
||||
label: string;
|
||||
value: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
error?: string;
|
||||
}) {
|
||||
const [draft, setDraft] = useState("");
|
||||
|
||||
const add = () => {
|
||||
const trimmed = draft.trim();
|
||||
if (!trimmed) return;
|
||||
if (value.includes(trimmed)) {
|
||||
setDraft("");
|
||||
return;
|
||||
}
|
||||
onChange([...value, trimmed]);
|
||||
setDraft("");
|
||||
};
|
||||
|
||||
const remove = (name: string) => {
|
||||
onChange(value.filter((v) => v !== name));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-navy mb-1.5">{label}</label>
|
||||
{value.length > 0 && (
|
||||
<ul className="flex flex-wrap gap-2 mb-2">
|
||||
{value.map((name) => (
|
||||
<li
|
||||
key={name}
|
||||
className="
|
||||
inline-flex items-center gap-1.5 rounded-full
|
||||
bg-gold-wash text-gold-deep border border-gold/40
|
||||
px-3 py-1 text-sm
|
||||
"
|
||||
>
|
||||
<span>{name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(name)}
|
||||
className="hover:text-danger transition-colors"
|
||||
aria-label={`הסר ${name}`}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
add();
|
||||
}
|
||||
}}
|
||||
placeholder="שם מלא של הצד"
|
||||
dir="rtl"
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm" onClick={add}>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{error && <p className="text-[0.72rem] text-danger mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user