Phase 4a: shadcn form primitives + case inline edit
Add dialog/select/textarea/label/progress/sonner components and wire
a Toaster into Providers. New zod schemas in lib/schemas/case.ts
mirror CaseCreateRequest/CaseUpdateRequest and feed react-hook-form
validation.
CaseEditDialog on the case detail Actions tab posts PUT /api/cases/{n}
with optimistic cache patching via useUpdateCase, showing toast
feedback on success/error.
shadcn's <Form> registry entry skipped at init (missing from the
nova preset); the dialog uses RHF directly against the same Input/
Textarea/Select primitives.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,9 @@
|
||||
* surfaces as a runtime TypeScript error the first time a property is touched.
|
||||
*/
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "./client";
|
||||
import type { CaseCreateInput, CaseUpdateInput } from "@/lib/schemas/case";
|
||||
|
||||
export type CaseStatus =
|
||||
| "new"
|
||||
@@ -95,6 +96,41 @@ export type WorkflowStatus = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export function useCreateCase() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: CaseCreateInput) =>
|
||||
apiRequest<{ case_number?: string; message?: string; [k: string]: unknown }>(
|
||||
`/api/cases/create`,
|
||||
{ method: "POST", body: input },
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: casesKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCase(caseNumber: string | undefined) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: CaseUpdateInput) =>
|
||||
apiRequest<CaseDetail>(`/api/cases/${caseNumber}`, {
|
||||
method: "PUT",
|
||||
body: input,
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
/* Patch cached detail and nudge the list to refetch on next focus */
|
||||
if (caseNumber) {
|
||||
qc.setQueryData<CaseDetail | undefined>(
|
||||
casesKeys.detail(caseNumber),
|
||||
(prev) => (prev ? { ...prev, ...data } : prev),
|
||||
);
|
||||
}
|
||||
qc.invalidateQueries({ queryKey: casesKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkflowStatus(caseNumber: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...casesKeys.all, "workflow", caseNumber ?? ""] as const,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { makeQueryClient } from "@/lib/api/client";
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,9 @@ import { makeQueryClient } from "@/lib/api/client";
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
const [queryClient] = useState(() => makeQueryClient());
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Toaster position="top-left" richColors closeButton dir="rtl" />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
82
web-ui/src/lib/schemas/case.ts
Normal file
82
web-ui/src/lib/schemas/case.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/* Appeal numbers follow the 1xxx / 8xxx / 9xxx convention from CLAUDE.md —
|
||||
* permissive regex that still catches obvious typos. */
|
||||
const caseNumberRe = /^[1-9]\d{3,}(?:[-/][\w\u0590-\u05FF]+)*$/;
|
||||
|
||||
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 committeeTypes = [
|
||||
"ועדה מקומית",
|
||||
"ועדה מחוזית",
|
||||
"ועדת ערר",
|
||||
] as const;
|
||||
|
||||
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, "מספר תיק לא תקין (למשל 1234 או 8001/2026)"),
|
||||
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).default(""),
|
||||
property_address: z.string().trim().max(200).default(""),
|
||||
permit_number: z.string().trim().max(100).default(""),
|
||||
committee_type: z.enum(committeeTypes).default("ועדה מקומית"),
|
||||
hearing_date: dateString.default(""),
|
||||
notes: z.string().trim().max(2000).default(""),
|
||||
expected_outcome: z
|
||||
.enum(expectedOutcomes.map((o) => o.value) as [string, ...string[]])
|
||||
.default(""),
|
||||
});
|
||||
|
||||
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>;
|
||||
Reference in New Issue
Block a user