/** * 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; /* 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;