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>
161 lines
5.6 KiB
TypeScript
161 lines
5.6 KiB
TypeScript
"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>
|
||
);
|
||
}
|