Files
legal-ai/web-ui/src/components/cases/case-edit-dialog.tsx
Chaim 2b591f5018
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
fix(case-page): resolve full-page review findings (bugs + RTL + resilience)
Full code review of the case-detail page (14 components) surfaced these,
all fixed here:

Logic bugs
- case-edit-dialog: form.reset ran on the 5s useCase refetch while the dialog
  was open, clobbering in-progress edits. Now resets only on open→true.
- status-changer: `selected` never synced to async/external `currentStatus`
  (stale dropdown). Reworked to track currentStatus until an explicit pick;
  resets to tracking after save.
- decision-blocks-panel: `block.content`/`word_count` accessed without null
  guards (endpoint has no response model) → potential render crash. Coerced
  with `?? ""` / `?? 0`. `STATUS_LABELS[status]` now falls back to the raw
  status instead of rendering literal "undefined".
- document-type-editor: `await mutateAsync()` in async click handlers without
  try/catch → unhandled promise rejection. Wrapped (errors still surface via
  isError).

Resilience / hygiene
- page.tsx: a transient 5xx on the background poll flipped the WHOLE page to
  the error card and discarded loaded data. Now gated on `!data`, plus a
  "נסה שוב" retry.
- cases.ts useUpdateCase: invalidated casesKeys.all, which re-invalidated the
  detail it had just optimistically patched. Scoped to the list prefix.

RTL correctness (logical properties)
- agent-status-widget `mr-auto`→`ms-auto`; agent-activity-feed `mr-auto`→
  `me-auto`, icon `ml-1/ml-2`→`me-1/me-2`, required `*` `mr-1`→`ms-1`;
  document-type-editor list `pr-4`→`ps-4`.

Minors
- drafts-panel: `<a href>`→`next/link` (operations + citation links) for SPA
  nav. agent-activity-feed: issueMap memoized; comment Textarea aria-label.
  upload-sheet: `unknown` status no longer shown as green success (neutral
  icon + "רענן לאישור"). citations.ts: case_name typed `string | null`.

Design gate: visual-touching items (RTL gap side, retry button, neutral
upload icon) were chair-authorized via the reviewed-findings approval ("fix
all"); none alter an approved page layout — they are correctness fixes.
tsc clean; eslint clean (1 pre-existing form.watch warning, untouched line).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:57:43 +00:00

259 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { 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, proceedingTypes,
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 ?? "",
proceeding_type: data.proceeding_type ?? "ערר",
},
});
/* Reset to the latest case values only on the open→true transition.
* Resetting on every `data` change would clobber in-progress edits, because
* useCase refetches every 5s (refetchInterval) while the dialog is open. */
const handleOpenChange = (next: boolean) => {
if (next) {
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 ?? "",
proceeding_type: data.proceeding_type ?? "ערר",
});
}
setOpen(next);
};
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={handleOpenChange}>
<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>
<Label className="text-navy">סוג תיק</Label>
<Controller
control={form.control}
name="proceeding_type"
render={({ field }) => (
<Select
value={field.value ?? "ערר"}
onValueChange={(v) =>
field.onChange(v as CaseUpdateInput["proceeding_type"])
}
dir="rtl"
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{proceedingTypes.map((p) => (
<SelectItem key={p.value} value={p.value}>
{p.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<p className="text-[0.7rem] text-ink-muted mt-1">
ערר = הליך עיקרי; בל&quot;מ = בקשה להארכת מועד להגשת ערר
</p>
</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>
);
}