From b67dc47dc789d841507e4019d41ab240416aceca Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 11 Apr 2026 16:23:37 +0000 Subject: [PATCH] Phase 4b: Case create wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- web-ui/src/app/cases/new/page.tsx | 26 ++ web-ui/src/app/page.tsx | 5 + web-ui/src/components/wizard/case-wizard.tsx | 283 ++++++++++++++++++ .../src/components/wizard/parties-field.tsx | 89 ++++++ web-ui/src/lib/schemas/case.ts | 18 +- 5 files changed, 412 insertions(+), 9 deletions(-) create mode 100644 web-ui/src/app/cases/new/page.tsx create mode 100644 web-ui/src/components/wizard/case-wizard.tsx create mode 100644 web-ui/src/components/wizard/parties-field.tsx diff --git a/web-ui/src/app/cases/new/page.tsx b/web-ui/src/app/cases/new/page.tsx new file mode 100644 index 0000000..3fa0d32 --- /dev/null +++ b/web-ui/src/app/cases/new/page.tsx @@ -0,0 +1,26 @@ +import Link from "next/link"; +import { AppShell } from "@/components/app-shell"; +import { CaseWizard } from "@/components/wizard/case-wizard"; + +export default function NewCasePage() { + return ( + +
+
+ +

יצירת תיק ערר

+

+ שלושה שלבים קצרים — פרטי יסוד, צדדים, והשלמות. התיק ייווצר ב-DB + וב-Gitea מיד בשמירה. +

+
+ + +
+
+ ); +} diff --git a/web-ui/src/app/page.tsx b/web-ui/src/app/page.tsx index a8e3992..607232b 100644 --- a/web-ui/src/app/page.tsx +++ b/web-ui/src/app/page.tsx @@ -1,10 +1,12 @@ "use client"; +import Link from "next/link"; import { AppShell } from "@/components/app-shell"; import { KPICards } from "@/components/cases/kpi-cards"; import { StatusDonut } from "@/components/cases/status-donut"; import { CasesTable } from "@/components/cases/cases-table"; import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; import { useCases } from "@/lib/api/cases"; export default function HomePage() { @@ -24,6 +26,9 @@ export default function HomePage() { 12 הבלוקים.

+
diff --git a/web-ui/src/components/wizard/case-wizard.tsx b/web-ui/src/components/wizard/case-wizard.tsx new file mode 100644 index 0000000..30b89fe --- /dev/null +++ b/web-ui/src/components/wizard/case-wizard.tsx @@ -0,0 +1,283 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "sonner"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from "@/components/ui/select"; +import { PartiesField } from "@/components/wizard/parties-field"; +import { useCreateCase } from "@/lib/api/cases"; +import { + caseCreateSchema, committeeTypes, expectedOutcomes, + type CaseCreateInput, +} from "@/lib/schemas/case"; + +const STEPS = [ + { key: "basics", label: "פרטי יסוד" }, + { key: "parties", label: "צדדים" }, + { key: "details", label: "השלמות" }, +] as const; + +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", "committee_type"], + parties: ["appellants", "respondents"], + details: ["subject", "hearing_date", "expected_outcome", "notes", "property_address", "permit_number"], +}; + +function FieldError({ message }: { message?: string }) { + if (!message) return null; + return

{message}

; +} + +export function CaseWizard() { + const router = useRouter(); + const [step, setStep] = useState("basics"); + const mutate = useCreateCase(); + + const form = useForm({ + resolver: zodResolver(caseCreateSchema), + mode: "onBlur", + defaultValues: { + case_number: "", + title: "", + appellants: [], + respondents: [], + subject: "", + property_address: "", + permit_number: "", + committee_type: "ועדה מקומית", + hearing_date: "", + notes: "", + expected_outcome: "", + }, + }); + + const stepIndex = STEPS.findIndex((s) => s.key === step); + const isLast = stepIndex === STEPS.length - 1; + + const goNext = async () => { + const ok = await form.trigger(STEP_FIELDS[step]); + if (!ok) return; + setStep(STEPS[stepIndex + 1].key); + }; + const goBack = () => setStep(STEPS[stepIndex - 1].key); + + const onSubmit = form.handleSubmit(async (values) => { + try { + const res = await mutate.mutateAsync(values); + toast.success("תיק חדש נוצר"); + const created = res?.case_number || values.case_number; + router.push(`/cases/${encodeURIComponent(created)}`); + } catch (e) { + toast.error(e instanceof Error ? e.message : "שגיאה ביצירת תיק"); + } + }); + + return ( + + + {/* Stepper */} +
    + {STEPS.map((s, i) => { + const active = i === stepIndex; + const done = i < stepIndex; + return ( +
  1. + + {done ? "✓" : i + 1} + + + {s.label} + + {i < STEPS.length - 1 && ( + + )} +
  2. + ); + })} +
+ +
+ {step === "basics" && ( +
+
+ + + +
+
+ + + +
+
+ + ( + + )} + /> +
+
+ )} + + {step === "parties" && ( +
+ ( + + )} + /> +
+ ( + + )} + /> +
+ )} + + {step === "details" && ( +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+ + ( + + )} + /> +
+
+
+ +