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:
160
web-ui/src/components/cases/case-edit-dialog.tsx
Normal file
160
web-ui/src/components/cases/case-edit-dialog.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter,
|
||||
DialogHeader, DialogTitle, DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
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 { useUpdateCase } from "@/lib/api/cases";
|
||||
import { caseUpdateSchema, expectedOutcomes, type CaseUpdateInput } from "@/lib/schemas/case";
|
||||
import type { CaseDetail } from "@/lib/api/cases";
|
||||
|
||||
/*
|
||||
* Inline edit dialog for core case fields. Uses react-hook-form + zod
|
||||
* directly (shadcn's <Form> registry entry wasn't available at init
|
||||
* time, so the styling is reproduced by hand in a lean form layout).
|
||||
*/
|
||||
|
||||
function FieldError({ message }: { message?: string }) {
|
||||
if (!message) return null;
|
||||
return <p className="text-[0.72rem] text-danger mt-1">{message}</p>;
|
||||
}
|
||||
|
||||
export function CaseEditDialog({ data }: { data: CaseDetail }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const mutate = useUpdateCase(data.case_number);
|
||||
|
||||
const form = useForm<CaseUpdateInput>({
|
||||
resolver: zodResolver(caseUpdateSchema),
|
||||
defaultValues: {
|
||||
title: data.title ?? "",
|
||||
subject: data.subject ?? "",
|
||||
hearing_date: data.hearing_date ?? "",
|
||||
notes: "",
|
||||
expected_outcome: data.expected_outcome ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
/* Re-sync the form when the underlying case refetches after save */
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
form.reset({
|
||||
title: data.title ?? "",
|
||||
subject: data.subject ?? "",
|
||||
hearing_date: data.hearing_date ?? "",
|
||||
notes: "",
|
||||
expected_outcome: data.expected_outcome ?? "",
|
||||
});
|
||||
}, [open, data, form]);
|
||||
|
||||
const onSubmit = form.handleSubmit(async (values) => {
|
||||
try {
|
||||
await mutate.mutateAsync(values);
|
||||
toast.success("פרטי התיק עודכנו");
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "שגיאה בעדכון התיק");
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
עריכת פרטי תיק
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg" dir="rtl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>עריכת פרטי תיק {data.case_number}</DialogTitle>
|
||||
<DialogDescription className="text-ink-muted">
|
||||
השינויים נשמרים ישירות ל-FastAPI. השדות הריקים נשארים ללא שינוי.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="title" className="text-navy">כותרת</Label>
|
||||
<Input id="title" {...form.register("title")} className="mt-1" />
|
||||
<FieldError message={form.formState.errors.title?.message} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="subject" className="text-navy">נושא</Label>
|
||||
<Input id="subject" {...form.register("subject")} className="mt-1" />
|
||||
<FieldError message={form.formState.errors.subject?.message} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="hearing_date" className="text-navy">תאריך דיון</Label>
|
||||
<Input
|
||||
id="hearing_date"
|
||||
type="date"
|
||||
{...form.register("hearing_date")}
|
||||
className="mt-1 tabular-nums"
|
||||
/>
|
||||
<FieldError message={form.formState.errors.hearing_date?.message} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-navy">תוצאה צפויה</Label>
|
||||
<Select
|
||||
value={form.watch("expected_outcome") || "__none__"}
|
||||
onValueChange={(v) =>
|
||||
form.setValue("expected_outcome", v === "__none__" ? "" : v)
|
||||
}
|
||||
dir="rtl"
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{expectedOutcomes.map((o) => (
|
||||
<SelectItem key={o.value || "none"} value={o.value || "__none__"}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="notes" className="text-navy">הערות (יתווספו לקיים)</Label>
|
||||
<Textarea id="notes" rows={3} {...form.register("notes")} className="mt-1" />
|
||||
<FieldError message={form.formState.errors.notes?.message} />
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={mutate.isPending}
|
||||
>
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={mutate.isPending}
|
||||
className="bg-navy hover:bg-navy-soft text-parchment"
|
||||
>
|
||||
{mutate.isPending ? "שומר…" : "שמור שינויים"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user