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>
95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
/**
|
|
* Zod schemas for case mutations (create / update).
|
|
* Shapes mirror the FastAPI Pydantic models in web/app.py:
|
|
* - CaseCreateRequest
|
|
* - CaseUpdateRequest
|
|
*
|
|
* Validation rules are stricter on the UI than on the backend so the user
|
|
* gets Hebrew-localized error messages at the field level instead of a
|
|
* 422 blob from FastAPI.
|
|
*/
|
|
|
|
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:
|
|
* NNNN-YY → "1033-25" (case sequence + 2-digit year)
|
|
* NNNN-MM-YY → "1000-04-26" (case sequence + 2-digit month + year)
|
|
*
|
|
* Slashes are deliberately forbidden: FastAPI path routing can't capture
|
|
* a `/` inside a {case_number} segment even when URL-encoded as %2F, so
|
|
* any case with a slash becomes unreachable at
|
|
* GET /api/cases/{case_number}/details. */
|
|
const caseNumberRe = /^[1-9]\d{3}(?:-\d{2}){1,2}$/;
|
|
|
|
const hebrewPartyRe = /[\u0590-\u05FFA-Za-z]/;
|
|
|
|
const dateString = z
|
|
.string()
|
|
.trim()
|
|
.refine((v) => v === "" || /^\d{4}-\d{2}-\d{2}$/.test(v), {
|
|
message: "תאריך חייב להיות בפורמט YYYY-MM-DD",
|
|
});
|
|
|
|
export const expectedOutcomes = [
|
|
{ value: "", label: "— לא נקבע —" },
|
|
{ value: "rejection", label: "דחייה" },
|
|
{ value: "partial_acceptance", label: "קבלה חלקית" },
|
|
{ value: "full_acceptance", label: "קבלה מלאה" },
|
|
{ value: "betterment_levy", label: "היטל השבחה" },
|
|
] as const;
|
|
|
|
export const caseCreateSchema = z.object({
|
|
case_number: z
|
|
.string()
|
|
.trim()
|
|
.min(1, "שדה חובה")
|
|
.regex(caseNumberRe, "פורמט: NNNN-YY או NNNN-MM-YY (למשל 1033-25 או 1000-04-26)"),
|
|
title: z.string().trim().min(3, "כותרת קצרה מדי").max(200, "כותרת ארוכה מדי"),
|
|
appellants: z
|
|
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
|
|
.min(1, "חייב להיות לפחות עורר אחד"),
|
|
respondents: z
|
|
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
|
|
.min(1, "חייב להיות לפחות משיב אחד"),
|
|
subject: z.string().trim().max(500),
|
|
property_address: z.string().trim().max(200),
|
|
permit_number: z.string().trim().max(100),
|
|
hearing_date: dateString,
|
|
notes: z.string().trim().max(2000),
|
|
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<typeof caseCreateSchema>;
|
|
|
|
/* Update schema — all fields optional so the PUT can be used for a
|
|
* single-field edit. Empty strings are tolerated (they mean "no change"
|
|
* in the Pydantic model). */
|
|
export const caseUpdateSchema = z.object({
|
|
title: z.string().trim().max(200).optional(),
|
|
subject: z.string().trim().max(500).optional(),
|
|
notes: z.string().trim().max(2000).optional(),
|
|
hearing_date: dateString.optional(),
|
|
decision_date: dateString.optional(),
|
|
expected_outcome: z
|
|
.enum(expectedOutcomes.map((o) => o.value) as [string, ...string[]])
|
|
.optional(),
|
|
status: z.string().optional(),
|
|
});
|
|
|
|
export type CaseUpdateInput = z.infer<typeof caseUpdateSchema>;
|