fix(#77 frontend): separate מספר-תיק field on committee upload + editable case_number in edit sheet

Pairs with the backend PR. Stops the citation (מראה-מקום) from being stored
as the identifier, and lets a wrong identifier be corrected after the fact.

- upload sheet: new required 'מספר תיק (מזהה ייחודי)' field for committee
  decisions → sent as case_number; the citation field is now sent as the
  separate citation (→ citation_formatted) instead of as case_number.
- edit sheet: the case_number block is now an editable input (was read-only).
  Halachot/chunks key off case_law_id (UUID), so renaming case_number is safe.
- precedent-library.ts: InternalDecisionUploadInput += citation; PrecedentPatch
  += case_number.
- types.ts: regenerated (api:types) — PrecedentUpdateRequest now carries
  case_number.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:17:16 +00:00
parent fc0c36b2f8
commit 7be1c3162c
4 changed files with 1065 additions and 24 deletions

View File

@@ -35,7 +35,7 @@ type Props = {
* each time the sheet opens so the form reflects any auto-fill that * each time the sheet opens so the form reflects any auto-fill that
* happened in the background. */ * happened in the background. */
type FormState = { type FormState = {
citation: string; case_number: string;
citation_formatted: string; citation_formatted: string;
case_name: string; case_name: string;
court: string; court: string;
@@ -54,7 +54,7 @@ type FormState = {
}; };
const EMPTY: FormState = { const EMPTY: FormState = {
citation: "", citation_formatted: "", case_number: "", citation_formatted: "",
case_name: "", court: "", district: "", chair_name: "", case_name: "", court: "", district: "", chair_name: "",
decision_date: "", practice_area: "", appeal_subtype: "", source_type: "", decision_date: "", practice_area: "", appeal_subtype: "", source_type: "",
precedent_level: "", is_binding: true, subject_tags: "", precedent_level: "", is_binding: true, subject_tags: "",
@@ -76,7 +76,7 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
if (record && record.id !== syncedRecordId) { if (record && record.id !== syncedRecordId) {
setSyncedRecordId(record.id as string); setSyncedRecordId(record.id as string);
setForm({ setForm({
citation: record.case_number || "", case_number: record.case_number || "",
citation_formatted: record.citation_formatted || "", citation_formatted: record.citation_formatted || "",
case_name: record.case_name || "", case_name: record.case_name || "",
court: record.court || "", court: record.court || "",
@@ -117,8 +117,10 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
key_quote: form.key_quote.trim(), key_quote: form.key_quote.trim(),
}; };
if (form.decision_date) patch.decision_date = form.decision_date; if (form.decision_date) patch.decision_date = form.decision_date;
// citation (case_number) is the unique key; we don't allow editing it // case_number is the canonical identifier. Editing it is safe —
// here to avoid orphaning halachot. To rename, delete + re-upload. // halachot/chunks reference case_law_id (UUID), not the case_number
// text — so a wrong id captured at upload can be corrected here.
if (form.case_number.trim()) patch.case_number = form.case_number.trim();
await update.mutateAsync({ id: caseLawId, patch }); await update.mutateAsync({ id: caseLawId, patch });
toast.success("נשמר"); toast.success("נשמר");
onOpenChange(false); onOpenChange(false);
@@ -148,7 +150,7 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
<DialogHeader className="px-6 pt-6"> <DialogHeader className="px-6 pt-6">
<DialogTitle className="text-navy">עריכת פרטי פסיקה</DialogTitle> <DialogTitle className="text-navy">עריכת פרטי פסיקה</DialogTitle>
<DialogDescription className="text-ink-muted"> <DialogDescription className="text-ink-muted">
כל השדות ניתנים לעריכה חוץ ממספר התיק (מזהה ייחודי במערכת). כל השדות ניתנים לעריכה, כולל מספר התיק (המזהה הייחודי).
כפתור &quot;חלץ מטא-דאטה&quot; שולח בקשה לתור מקומי שאני מרוקן כפתור &quot;חלץ מטא-דאטה&quot; שולח בקשה לתור מקומי שאני מרוקן
מ-Claude Code (ה-LLM רץ מקומית עם <code>claude session</code>, מ-Claude Code (ה-LLM רץ מקומית עם <code>claude session</code>,
לא ב-API). לא ב-API).
@@ -162,12 +164,17 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
) : ( ) : (
<> <>
<form onSubmit={onSubmit} className="px-6 pb-6 space-y-4 mt-4"> <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="rounded-lg border border-rule bg-rule-soft/40 p-3 flex items-end gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 space-y-1">
<div className="text-[0.78rem] text-ink-muted">מספר תיק (מזהה ייחודי לא ניתן לעריכה)</div> <Label htmlFor="case-number" className="text-[0.78rem] text-ink-muted">
<div className="text-navy font-mono text-sm break-all" dir="ltr"> מספר תיק (מזהה ייחודי)
{record.case_number} </Label>
</div> <Input
id="case-number" value={form.case_number}
onChange={(e) => setForm({ ...form, case_number: e.target.value })}
className="font-mono text-sm" dir="ltr"
placeholder="8027-25"
/>
</div> </div>
<Button <Button
type="button" size="sm" variant="outline" type="button" size="sm" variant="outline"

View File

@@ -50,6 +50,10 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
// require chair_name + district. Routing is by citation prefix. // require chair_name + district. Routing is by citation prefix.
const [chairName, setChairName] = useState(""); const [chairName, setChairName] = useState("");
const [district, setDistrict] = useState<CommitteeDistrict | "">(""); const [district, setDistrict] = useState<CommitteeDistrict | "">("");
// Canonical identifier for committee decisions (e.g. "8027-25"), kept
// distinct from the citation (מראה-מקום). Previously the citation was sent
// as case_number, polluting the identifier.
const [caseNumber, setCaseNumber] = useState("");
const isCommittee = isCommitteeCitation(citation); const isCommittee = isCommitteeCitation(citation);
const [taskId, setTaskId] = useState<string | null>(null); const [taskId, setTaskId] = useState<string | null>(null);
@@ -72,7 +76,7 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
setHeadnote(""); setIsBinding(true); setTaskId(null); setHeadnote(""); setIsBinding(true); setTaskId(null);
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
setChairName(""); setDistrict(""); setChairName(""); setDistrict(""); setCaseNumber("");
}, [open]); }, [open]);
// Auto-close on completion + refresh library list/stats so the new // Auto-close on completion + refresh library list/stats so the new
@@ -110,6 +114,10 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
try { try {
if (isCommittee) { if (isCommittee) {
if (!caseNumber.trim()) {
toast.error("מספר תיק (מזהה ייחודי) חובה להחלטת ועדת ערר");
return;
}
if (!chairName.trim()) { if (!chairName.trim()) {
toast.error("שם יו\"ר חובה להחלטת ועדת ערר"); toast.error("שם יו\"ר חובה להחלטת ועדת ערר");
return; return;
@@ -120,7 +128,8 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
} }
const res = await uploadInternal.mutateAsync({ const res = await uploadInternal.mutateAsync({
file, file,
case_number: citation.trim(), case_number: caseNumber.trim(),
citation: citation.trim(),
chair_name: chairName.trim(), chair_name: chairName.trim(),
district, district,
case_name: caseName.trim(), case_name: caseName.trim(),
@@ -202,7 +211,20 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
</div> </div>
{isCommittee && ( {isCommittee && (
<div className="grid grid-cols-2 gap-3 rounded-md border border-gold/40 bg-gold-wash/40 p-3"> <div className="space-y-3 rounded-md border border-gold/40 bg-gold-wash/40 p-3">
<div className="space-y-1">
<Label htmlFor="committee-case-number">מספר תיק מזהה ייחודי (חובה)</Label>
<Input
id="committee-case-number" value={caseNumber}
onChange={(e) => setCaseNumber(e.target.value)}
placeholder="8027-25"
disabled={isProcessing} dir="rtl"
/>
<p className="text-[0.72rem] text-ink-muted">
המזהה הנקי בלבד (למשל 8027-25) לא המראה-מקום המלא.
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1"> <div className="space-y-1">
<Label htmlFor="chair-name">שם יו&quot;ר (חובה)</Label> <Label htmlFor="chair-name">שם יו&quot;ר (חובה)</Label>
<Input <Input
@@ -231,6 +253,7 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
</Select> </Select>
</div> </div>
</div> </div>
</div>
)} )}
<details className="group rounded-md border border-rule bg-rule-soft/30"> <details className="group rounded-md border border-rule bg-rule-soft/30">

View File

@@ -387,6 +387,9 @@ export function isCommitteeCitation(citation: string): boolean {
export type InternalDecisionUploadInput = { export type InternalDecisionUploadInput = {
file: File; file: File;
case_number: string; case_number: string;
/** Full citation (מראה-מקום), stored as citation_formatted. Distinct from
* the canonical case_number identifier. */
citation?: string;
chair_name: string; chair_name: string;
district: CommitteeDistrict | string; district: CommitteeDistrict | string;
case_name?: string; case_name?: string;
@@ -406,6 +409,7 @@ export function useUploadInternalDecision() {
const fd = new FormData(); const fd = new FormData();
fd.append("file", input.file); fd.append("file", input.file);
fd.append("case_number", input.case_number); fd.append("case_number", input.case_number);
if (input.citation) fd.append("citation", input.citation);
fd.append("chair_name", input.chair_name); fd.append("chair_name", input.chair_name);
fd.append("district", input.district); fd.append("district", input.district);
if (input.case_name) fd.append("case_name", input.case_name); if (input.case_name) fd.append("case_name", input.case_name);
@@ -488,6 +492,7 @@ export function useUnlinkRelatedCase(caseId: string) {
} }
export type PrecedentPatch = Partial<{ export type PrecedentPatch = Partial<{
case_number: string;
case_name: string; case_name: string;
court: string; court: string;
decision_date: string; decision_date: string;

File diff suppressed because it is too large Load Diff