Files
legal-ai/web-ui/src/components/precedents/precedent-edit-sheet.tsx
Chaim d81c3c37ab
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 34s
fix(precedent-edit): translate appeal_subtype enum values to Hebrew
The metadata extractor occasionally stuffs the practice_area enum
(``betterment_levy``, ``rishuy_uvniya``, ``compensation_197``) into
the free-text ``appeal_subtype`` column. The edit sheet then showed the
raw English string in the "תת-סוג" input.

When initialising the form, run the value through ``appealSubtypeLabel``
which maps known practice-area enum values to their Hebrew label and
returns anything else unchanged. The user can then edit normally; on
save the Hebrew sticks, so the next view is also clean.
2026-05-07 08:45:03 +00:00

292 lines
12 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 { useEffect, useState } from "react";
import { Save, Sparkles } from "lucide-react";
import { toast } from "sonner";
import {
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import {
usePrecedent,
useUpdatePrecedent,
useRequestMetadataExtraction,
type PracticeArea,
type SourceType,
} from "@/lib/api/precedent-library";
import {
PRACTICE_AREAS, PRECEDENT_LEVELS, SOURCE_TYPES, appealSubtypeLabel,
} from "./practice-area";
import { ExtractedHalachotSection } from "./extracted-halachot";
type Props = {
caseLawId: string | null;
onOpenChange: (open: boolean) => void;
};
/* All editable fields. Pulled fresh from /api/precedent-library/{id}
* each time the sheet opens so the form reflects any auto-fill that
* happened in the background. */
type FormState = {
citation: string;
case_name: string;
court: string;
decision_date: string;
practice_area: PracticeArea;
appeal_subtype: string;
source_type: SourceType;
precedent_level: string;
is_binding: boolean;
subject_tags: string;
summary: string;
headnote: string;
key_quote: string;
};
const EMPTY: FormState = {
citation: "", case_name: "", court: "", decision_date: "",
practice_area: "", appeal_subtype: "", source_type: "",
precedent_level: "", is_binding: true, subject_tags: "",
summary: "", headnote: "", key_quote: "",
};
export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
const open = caseLawId !== null;
const { data: record, isPending } = usePrecedent(caseLawId);
const update = useUpdatePrecedent();
const requestMetadata = useRequestMetadataExtraction();
const [form, setForm] = useState<FormState>(EMPTY);
// Hydrate form when the record loads.
useEffect(() => {
if (!record) return;
// eslint-disable-next-line react-hooks/set-state-in-effect
setForm({
citation: record.case_number || "",
case_name: record.case_name || "",
court: record.court || "",
decision_date: record.date ? record.date.slice(0, 10) : "",
practice_area: (record.practice_area || "") as PracticeArea,
appeal_subtype: appealSubtypeLabel(record.appeal_subtype),
source_type: (record.source_type || "") as SourceType,
precedent_level: record.precedent_level || "",
is_binding: record.is_binding ?? true,
subject_tags: (record.subject_tags || []).join(", "),
summary: record.summary || "",
headnote: record.headnote || "",
key_quote: (record as { key_quote?: string }).key_quote || "",
});
}, [record]);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!caseLawId) return;
try {
const patch: Record<string, unknown> = {
case_name: form.case_name.trim(),
court: form.court.trim(),
practice_area: form.practice_area || undefined,
appeal_subtype: form.appeal_subtype.trim(),
source_type: form.source_type || undefined,
precedent_level: form.precedent_level || undefined,
is_binding: form.is_binding,
subject_tags: form.subject_tags
.split(",").map((t) => t.trim()).filter(Boolean),
summary: form.summary.trim(),
headnote: form.headnote.trim(),
key_quote: form.key_quote.trim(),
};
if (form.decision_date) patch.decision_date = form.decision_date;
// citation (case_number) is the unique key; we don't allow editing it
// here to avoid orphaning halachot. To rename, delete + re-upload.
await update.mutateAsync({ id: caseLawId, patch });
toast.success("נשמר");
onOpenChange(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : "שגיאה");
}
};
const onRequestMetadata = async () => {
if (!caseLawId) return;
try {
await requestMetadata.mutateAsync(caseLawId);
toast.success(
"סומן לחילוץ מטא-דאטה. הריצי מ-Claude Code: precedent_process_pending",
);
} catch (err) {
toast.error(err instanceof Error ? err.message : "שגיאה");
}
};
return (
<Sheet open={open} onOpenChange={(o) => { if (!o) onOpenChange(false); }}>
<SheetContent side="left" className="w-full sm:max-w-2xl overflow-y-auto" dir="rtl">
<SheetHeader>
<SheetTitle className="text-navy">עריכת פרטי פסיקה</SheetTitle>
<SheetDescription className="text-ink-muted">
כל השדות ניתנים לעריכה חוץ ממראה המקום (מזהה ייחודי).
כפתור &quot;חלץ מטא-דאטה&quot; שולח בקשה לתור מקומי שאני מרוקן
מ-Claude Code (ה-LLM רץ מקומית עם <code>claude session</code>,
לא ב-API).
</SheetDescription>
</SheetHeader>
{isPending || !record ? (
<div className="px-6 pb-6 mt-4 space-y-3">
{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}
</div>
) : (
<>
<form onSubmit={onSubmit} className="px-6 pb-6 space-y-4 mt-4">
<div className="rounded-lg border border-rule bg-rule-soft/40 p-3 flex items-start gap-3">
<div className="flex-1 min-w-0">
<div className="text-[0.78rem] text-ink-muted">מראה מקום (לא ניתן לעריכה)</div>
<div className="text-navy font-mono text-sm break-all" dir="ltr">
{record.case_number}
</div>
</div>
<Button
type="button" size="sm" variant="outline"
onClick={onRequestMetadata}
disabled={requestMetadata.isPending}
className="shrink-0"
title="שולח בקשה לחילוץ מטא-דאטה לתור המקומי"
>
<Sparkles className="w-3.5 h-3.5 me-1" />
חלץ מטא-דאטה
</Button>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label htmlFor="case-name">שם קצר</Label>
<Input id="case-name" value={form.case_name}
onChange={(e) => setForm({ ...form, case_name: e.target.value })}
placeholder="ערר 403/17 / אהרון ברק" />
</div>
<div className="space-y-1">
<Label htmlFor="court">ערכאה</Label>
<Input id="court" value={form.court}
onChange={(e) => setForm({ ...form, court: e.target.value })} />
</div>
<div className="space-y-1">
<Label htmlFor="date">תאריך</Label>
<Input id="date" type="date" value={form.decision_date}
onChange={(e) => setForm({ ...form, decision_date: e.target.value })} />
</div>
<div className="space-y-1">
<Label htmlFor="appeal-subtype">תת-סוג</Label>
<Input id="appeal-subtype" value={form.appeal_subtype}
onChange={(e) => setForm({ ...form, appeal_subtype: e.target.value })}
placeholder="תכנית רחביה / סופיות ההחלטה" />
</div>
</div>
<div className="space-y-1">
<Label>תחום</Label>
<div className="flex gap-4 flex-wrap">
{PRACTICE_AREAS.map((a) => (
<label key={a.value} className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="practice_area" value={a.value}
checked={form.practice_area === a.value}
onChange={() => setForm({ ...form, practice_area: a.value as PracticeArea })} />
<span className="text-sm">{a.label}</span>
</label>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label htmlFor="source-type">סוג מקור</Label>
<Select value={form.source_type || "_none"}
onValueChange={(v) => setForm({ ...form, source_type: v === "_none" ? "" : v as SourceType })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="_none"></SelectItem>
{SOURCE_TYPES.map((s) => (
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label htmlFor="precedent-level">רמת תקדים</Label>
<Select value={form.precedent_level || "_none"}
onValueChange={(v) => setForm({ ...form, precedent_level: v === "_none" ? "" : v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="_none"></SelectItem>
{PRECEDENT_LEVELS.map((l) => (
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="tags">תגיות נושא (מופרדות בפסיקים)</Label>
<Input id="tags" value={form.subject_tags}
onChange={(e) => setForm({ ...form, subject_tags: e.target.value })}
placeholder="חניה, קווי בניין, שיקול דעת" />
</div>
<div className="space-y-1">
<Label htmlFor="summary">תקציר (2-3 משפטים)</Label>
<Textarea id="summary" value={form.summary} rows={3} dir="rtl"
onChange={(e) => setForm({ ...form, summary: e.target.value })} />
</div>
<div className="space-y-1">
<Label htmlFor="headnote">Headnote (משפט-שניים)</Label>
<Textarea id="headnote" value={form.headnote} rows={2} dir="rtl"
onChange={(e) => setForm({ ...form, headnote: e.target.value })} />
</div>
<div className="space-y-1">
<Label htmlFor="key-quote">ציטוט מרכזי</Label>
<Textarea id="key-quote" value={form.key_quote} rows={3} dir="rtl"
onChange={(e) => setForm({ ...form, key_quote: e.target.value })} />
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={form.is_binding}
onChange={(e) => setForm({ ...form, is_binding: e.target.checked })} />
<span className="text-sm">הלכה מחייבת (binding)</span>
<span className="text-[0.7rem] text-ink-muted">
בדרך כלל רק עליון/מנהלי. ועדות ערר אחרות = לא מחייב.
</span>
</label>
<div className="flex gap-2 justify-end pt-2 border-t border-rule-soft">
<Button type="button" variant="ghost"
onClick={() => onOpenChange(false)} disabled={update.isPending}>
ביטול
</Button>
<Button type="submit" disabled={update.isPending}
className="bg-navy text-parchment hover:bg-navy-soft">
<Save className="w-4 h-4 me-1" />
שמור
</Button>
</div>
</form>
<div className="px-6 pb-8 pt-2 border-t border-rule">
<ExtractedHalachotSection halachot={record.halachot ?? []} />
</div>
</>
)}
</SheetContent>
</Sheet>
);
}