All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m38s
- Fix keyboard navigation bug: React was reusing the submit button DOM element when transitioning "הבא" → "צור תיק", retaining focus and causing Enter to auto-submit step 3. Added key props to force element replacement. - CaseEditDialog now covers all wizard fields: appellants, respondents, property_address, permit_number (in addition to existing title, subject, hearing_date, expected_outcome, notes). - When case title changes, Paperclip project name is updated in background via new update_project_name() in paperclip_client.py. - Extended CaseUpdateRequest, case_update MCP tool, and caseUpdateSchema to carry the new fields end-to-end. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
219 lines
7.5 KiB
TypeScript
219 lines
7.5 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { useForm, Controller } 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 { PartiesField } from "@/components/wizard/parties-field";
|
||
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 all case fields set at creation time.
|
||
* Uses react-hook-form + zod directly (shadcn's <Form> registry entry
|
||
* wasn't available at init time, so the styling is reproduced by hand).
|
||
*/
|
||
|
||
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 ?? "",
|
||
appellants: data.appellants ?? [],
|
||
respondents: data.respondents ?? [],
|
||
property_address: data.property_address ?? "",
|
||
permit_number: data.permit_number ?? "",
|
||
},
|
||
});
|
||
|
||
/* 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 ?? "",
|
||
appellants: data.appellants ?? [],
|
||
respondents: data.respondents ?? [],
|
||
property_address: data.property_address ?? "",
|
||
permit_number: data.permit_number ?? "",
|
||
});
|
||
}, [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 max-h-[90vh] overflow-y-auto" dir="rtl">
|
||
<DialogHeader>
|
||
<DialogTitle>עריכת פרטי תיק {data.case_number}</DialogTitle>
|
||
<DialogDescription className="text-ink-muted">
|
||
השינויים נשמרים ישירות ל-DB. שינוי כותרת יסנכרן גם ל-Paperclip.
|
||
</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="h-px bg-rule" />
|
||
|
||
<Controller
|
||
control={form.control}
|
||
name="appellants"
|
||
render={({ field, fieldState }) => (
|
||
<PartiesField
|
||
label="עוררים"
|
||
value={field.value ?? []}
|
||
onChange={field.onChange}
|
||
error={fieldState.error?.message}
|
||
/>
|
||
)}
|
||
/>
|
||
|
||
<Controller
|
||
control={form.control}
|
||
name="respondents"
|
||
render={({ field, fieldState }) => (
|
||
<PartiesField
|
||
label="משיבים"
|
||
value={field.value ?? []}
|
||
onChange={field.onChange}
|
||
error={fieldState.error?.message}
|
||
/>
|
||
)}
|
||
/>
|
||
|
||
<div className="h-px bg-rule" />
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label htmlFor="property_address" className="text-navy">כתובת הנכס</Label>
|
||
<Input
|
||
id="property_address"
|
||
{...form.register("property_address")}
|
||
className="mt-1"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="permit_number" className="text-navy">מס׳ תכנית/בקשה</Label>
|
||
<Input
|
||
id="permit_number"
|
||
{...form.register("permit_number")}
|
||
className="mt-1"
|
||
/>
|
||
</div>
|
||
</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>
|
||
);
|
||
}
|