Files
legal-ai/web-ui/src/app/precedents/[id]/page.tsx
Chaim f3b075d282
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (Claude Design) על כל
הדפים — שינוי-הרכב אמיתי פר-מוקאפ, לא ליטוש-טוקנים. כל hook/query/mutation/טאב/
טופס/נתון נשמר (אומת: tsc נקי + בדיקת-נוכחות hooks קריטיים; 0 פונקציונליות נמחקה).

דפים (← מוקאפ):
- בית — לוח: KPI + "תיקים לפי סטטוס" (bars) + כרטיס-אישורים + CTA כפול.
- ארכיון — filter-bar שטוח + טבלה נקייה + צ'יפי-סוג/תוצאה.
- הערות יו״ר — פריסה דו-טורית + טופס-הוספה חי + כרטיסי-הערה.
- ספריית-פסיקה — tabs קו-תחתון + כרטיסי-תוצאה halacha/קטע + AuthorityBadge.
- דף-תקדים — באנר-meta parchment + דו-טורי + provenance pills.
- פסיקה-חסרה — pill פתוחים + צ'יפי-סטטוס + CTA העלאה.
- יומונים — אזור-העלאה מקווקו + כרטיסי-digest + "ממתין" כתווית פסיבית.
- גרף — פאנל-צד שכבות/אנליטיקה + canvas parchment.
- אימון-סגנון — פורטרט: banner + KPI + אנטומיה + ביטויי-חתימה.
- מתודולוגיה — עורך-צ'קליסט + "חל על:" + canon chip.
- מיומנויות/סקריפטים — טבלאות אמיתיות + צ'יפי-סטטוס.
- הגדרות — sidenav דו-טורי + env-rows עם "ממתין ל-redeploy".
- דף-תיק — באנר-תיק parchment + tabs + timeline + "פתח עורך החלטה".
- תפעול — SectionHeaders + טבלת-שירותים + כרטיסי-שער gold-wash.
- compose — באנר-תיק + SOT pill + פריסה דו-טורית + "השלמה והעברה".

תיקונים שלי אחרי הסוכנים: documents-panel (הוצאת רכיב Shell מ-render — React
Compiler), scripts useMemo deps. /approvals כבר נבנה מחדש נאמנה (commit קודם).

בדיקות: npx tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109 קיים-מראש).
שימור-פונקציונליות אומת. CI Docker build = שער סופי לפני deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:00:25 +00:00

378 lines
14 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 { use, useState } from "react";
import Link from "next/link";
import { Pencil, Check, X, Share2 } from "lucide-react";
import { toast } from "sonner";
import { AppShell } from "@/components/app-shell";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import {
usePrecedent,
useUpdatePrecedent,
type Precedent,
} from "@/lib/api/precedent-library";
import { PrecedentEditSheet } from "@/components/precedents/precedent-edit-sheet";
import {
FormattedCitation,
CitationCopyButton,
} from "@/components/precedents/formatted-citation";
import { ExtractedHalachotSection } from "@/components/precedents/extracted-halachot";
import { RelatedCasesSection } from "@/components/precedents/link-related-dialog";
const PRACTICE_AREA_LABELS: Record<string, string> = {
rishuy_uvniya: "רישוי ובנייה",
betterment_levy: "היטל השבחה",
compensation_197: "פיצויים (197)",
};
const SOURCE_TYPE_LABELS: Record<string, string> = {
court_ruling: "פסק דין",
appeals_committee: "ועדת ערר",
};
/** label/value pair in the parchment meta-band (mockup 08 `.mb`). */
function MetaItem({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex flex-col gap-0.5">
<span className="text-[0.7rem] text-ink-muted font-medium">{label}</span>
<span className="text-[0.84rem] text-ink font-semibold">{children}</span>
</div>
);
}
/* Next 16 breaking change: route params are now a Promise.
* The `use()` hook unwraps them inside a client component. */
export default function PrecedentDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
const [editing, setEditing] = useState(false);
const { data, isPending, error } = usePrecedent(id);
const update = useUpdatePrecedent();
const [editingCitation, setEditingCitation] = useState(false);
const [citationDraft, setCitationDraft] = useState("");
if (error) {
return (
<AppShell>
<section className="space-y-6" dir="rtl">
<div className="rounded-lg border border-danger/40 bg-danger-bg px-6 py-6 text-center space-y-3">
<p className="text-danger font-semibold">שגיאה בטעינת הפסיקה</p>
<p className="text-sm text-ink-muted">{error.message}</p>
<Button asChild variant="outline">
<Link href="/precedents">חזרה לספרייה</Link>
</Button>
</div>
</section>
</AppShell>
);
}
if (isPending || !data) {
return (
<AppShell>
<section className="space-y-6" dir="rtl">
<Skeleton className="h-40 w-full" />
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
<div className="space-y-4">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-28 w-full" />)}
</div>
<Skeleton className="h-64 w-full" />
</div>
</section>
</AppShell>
);
}
const date = data.date ? data.date.slice(0, 10) : null;
return (
<AppShell>
<div dir="rtl">
{/* ── parchment header band (mockup 08 `.band`) — breaks out to the
AppShell <main> edges (px-10 py-10) for a full-width band. ──── */}
<div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-6">
<nav className="text-[0.78rem] text-ink-muted mb-2">
<Link href="/precedents" className="text-gold-deep hover:underline">פסיקה</Link>
<span aria-hidden> </span>
<Link href="/precedents" className="text-gold-deep hover:underline">ספרייה</Link>
<span aria-hidden> </span>
<span>תקדים</span>
</nav>
<div className="flex items-start justify-between gap-3 flex-wrap">
<div className="min-w-0">
<h1 className="text-navy text-2xl font-bold leading-snug mb-1">
{data.case_name || "—"}
</h1>
<div className="text-ink-soft text-sm font-mono" dir="ltr">
{data.case_number}
</div>
</div>
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm" className="border-rule">
<Link href={`/graph?focus=cl:${id}`}>
<Share2 className="w-3.5 h-3.5 me-1" /> הצג בגרף
</Link>
</Button>
<Button variant="outline" size="sm" className="border-rule" onClick={() => setEditing(true)}>
<Pencil className="w-3.5 h-3.5 me-1" /> ערוך פרטים
</Button>
</div>
</div>
{/* citation (unified Israeli citation rules) — chair-editable */}
<div className="mt-4 max-w-3xl">
<CitationBlock
precedent={data as Precedent}
editing={editingCitation}
draft={citationDraft}
onStartEdit={() => {
setCitationDraft(data.citation_formatted ?? "");
setEditingCitation(true);
}}
onCancel={() => setEditingCitation(false)}
onChange={setCitationDraft}
onSave={async () => {
try {
await update.mutateAsync({
id,
patch: { citation_formatted: citationDraft.trim() },
});
toast.success("מראה מקום עודכן");
setEditingCitation(false);
} catch (e) {
toast.error(e instanceof Error ? e.message : "שמירה נכשלה");
}
}}
saving={update.isPending}
/>
</div>
{/* meta-band — label/value pairs + chips (mockup 08 `.metaband`) */}
<div className="mt-4 flex flex-wrap items-center gap-x-6 gap-y-3">
{data.court ? <MetaItem label="בית-משפט">{data.court}</MetaItem> : null}
{date ? (
<MetaItem label="תאריך">
<span className="tabular-nums" dir="ltr">{date}</span>
</MetaItem>
) : null}
{data.practice_area ? (
<MetaItem label="תחום">
<Badge variant="outline" className="text-[0.72rem] bg-info-bg text-info border-transparent rounded-full px-3">
{PRACTICE_AREA_LABELS[data.practice_area] ?? data.practice_area}
</Badge>
</MetaItem>
) : null}
{data.source_type ? (
<MetaItem label="סוג-מקור">
<Badge variant="outline" className="text-[0.72rem] rounded-full px-3">
{SOURCE_TYPE_LABELS[data.source_type] ?? data.source_type}
</Badge>
</MetaItem>
) : null}
{data.precedent_level ? (
<MetaItem label="רמת-תקדים">
<Badge variant="outline" className="text-[0.72rem] bg-gold-wash text-gold-deep border-rule rounded-full px-3">
{data.precedent_level}
</Badge>
</MetaItem>
) : null}
{data.is_binding ? (
<MetaItem label="סיווג-מחייבות">
<Badge variant="outline" className="text-[0.72rem] bg-success-bg text-success border-transparent rounded-full px-3">
מחייב
</Badge>
</MetaItem>
) : null}
</div>
{data.subject_tags?.length ? (
<div className="mt-3 flex items-center gap-1 flex-wrap">
{data.subject_tags.map((t) => (
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
{t}
</Badge>
))}
</div>
) : null}
</div>
{/* ── two-column body (mockup 08 `.wrap` grid) ────────────────── */}
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
{/* main column */}
<div className="space-y-4">
{data.summary ? (
<DetailCard title="תקציר">
<p className="text-ink-soft text-sm leading-8 m-0 whitespace-pre-line">
{data.summary}
</p>
</DetailCard>
) : null}
{data.headnote ? (
<DetailCard title="כותרת-הלכה (headnote)" prov="opus">
<p className="text-ink-soft text-sm leading-8 m-0">{data.headnote}</p>
</DetailCard>
) : null}
{(data as { key_quote?: string }).key_quote ? (
<DetailCard title="ציטוט-מפתח" prov="opus">
<blockquote className="rounded-e border-s-[3px] border-gold bg-gold-wash px-4 py-3 text-sm text-ink-soft leading-8 m-0">
{(data as { key_quote?: string }).key_quote}
</blockquote>
</DetailCard>
) : null}
<div className="rounded-lg border border-rule bg-surface shadow-sm px-5 py-4">
<ExtractedHalachotSection halachot={data.halachot ?? []} />
</div>
</div>
{/* side rail — citations + corroboration */}
<div className="space-y-4">
<RelatedCasesSection caseId={id} related={data.related_cases ?? []} />
</div>
</div>
</div>
<PrecedentEditSheet
caseLawId={editing ? id : null}
onOpenChange={(open) => setEditing(open)}
/>
</AppShell>
);
}
/** main-column card with a navy heading + optional provenance pill
* ("מולא ע״י Opus", mockup 08 `.prov`). */
function DetailCard({
title,
prov,
children,
}: {
title: string;
prov?: "opus";
children: React.ReactNode;
}) {
return (
<div className="rounded-lg border border-rule bg-surface shadow-sm px-5 py-4">
<h2 className="text-navy text-[1.05rem] font-semibold mb-1.5 flex items-center gap-2.5 flex-wrap">
{title}
{prov === "opus" ? (
<span className="inline-flex items-center rounded-full bg-info-bg text-info text-[0.68rem] font-semibold px-2.5 py-0.5">
מולא ע״י Opus
</span>
) : null}
</h2>
{children}
</div>
);
}
function CitationBlock({
precedent,
editing,
draft,
onStartEdit,
onCancel,
onChange,
onSave,
saving,
}: {
precedent: Precedent;
editing: boolean;
draft: string;
onStartEdit: () => void;
onCancel: () => void;
onChange: (v: string) => void;
onSave: () => void;
saving: boolean;
}) {
const citation = (precedent.citation_formatted ?? "").trim();
if (editing) {
return (
<div className="rounded-md border border-gold/40 bg-gold-wash/40 p-3 space-y-2">
<div className="flex items-center justify-between gap-2">
<span className="text-[0.78rem] font-semibold text-navy">
עריכת מראה מקום
</span>
<span className="text-[0.7rem] text-ink-muted">
הקף את שמות הצדדים בכפול-כוכבית <code className="font-mono">**שם**</code> להדגשה
</span>
</div>
<Textarea
value={draft}
onChange={(e) => onChange(e.target.value)}
rows={3}
dir="rtl"
className="font-mono text-sm bg-surface"
placeholder='ערר (ועדות ערר ...) 1234/24 **עורר נ&apos; הוועדה המקומית** (נבו 1.2.2025)'
disabled={saving}
/>
<div className="flex items-center gap-2">
<Button
size="sm"
onClick={onSave}
disabled={saving || !draft.trim()}
className="bg-navy text-parchment hover:bg-navy-soft"
>
<Check className="w-3.5 h-3.5 me-1" />
שמור
</Button>
<Button size="sm" variant="outline" onClick={onCancel} disabled={saving}>
<X className="w-3.5 h-3.5 me-1" />
ביטול
</Button>
</div>
</div>
);
}
if (!citation) {
return (
<div className="rounded-md border border-dashed border-rule bg-surface/60 p-3 flex items-center justify-between gap-2">
<span className="text-[0.78rem] text-ink-muted">
מראה מקום (כללי הציטוט האחיד) טרם חולץ
</span>
<Button size="sm" variant="outline" onClick={onStartEdit}>
<Pencil className="w-3.5 h-3.5 me-1" />
הוסף ידנית
</Button>
</div>
);
}
return (
<div className="rounded-md border border-rule bg-surface p-3 space-y-1.5">
<div className="flex items-center justify-between gap-2">
<span className="text-[0.7rem] uppercase tracking-wide text-ink-muted">
מראה מקום
</span>
<div className="flex items-center gap-1.5">
<CitationCopyButton citation={citation} size="xs" />
<button
type="button"
onClick={onStartEdit}
title="ערוך מראה מקום"
aria-label="ערוך מראה מקום"
className="inline-flex items-center justify-center rounded-md border border-rule bg-surface hover:bg-rule-soft/50 text-ink-muted hover:text-navy h-7 w-7"
>
<Pencil className="w-3.5 h-3.5" />
</button>
</div>
</div>
<FormattedCitation
citation={citation}
className="block text-navy text-sm leading-relaxed"
/>
</div>
);
}