All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (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>
232 lines
8.3 KiB
TypeScript
232 lines
8.3 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Link2, Loader2, X } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
usePrecedents,
|
||
useLinkRelatedCase,
|
||
useUnlinkRelatedCase,
|
||
RelatedCase,
|
||
} from "@/lib/api/precedent-library";
|
||
|
||
const LEVEL_LABELS: Record<string, string> = {
|
||
"עליון": "עליון",
|
||
"מנהלי": "מנהלי",
|
||
"ועדת_ערר_ארצית": "ארצי",
|
||
"ועדת_ערר_מחוזית": "מחוזי",
|
||
};
|
||
|
||
const LEVEL_COLORS: Record<string, string> = {
|
||
"עליון": "bg-red-50 text-red-700 border-red-200",
|
||
"מנהלי": "bg-orange-50 text-orange-700 border-orange-200",
|
||
"ועדת_ערר_ארצית": "bg-blue-50 text-blue-700 border-blue-200",
|
||
"ועדת_ערר_מחוזית": "bg-green-50 text-green-700 border-green-200",
|
||
};
|
||
|
||
// ── Search Dialog ────────────────────────────────────────────────────
|
||
|
||
type DialogProps = {
|
||
caseId: string;
|
||
currentRelated: RelatedCase[];
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
};
|
||
|
||
function LinkDialog({ caseId, currentRelated, open, onOpenChange }: DialogProps) {
|
||
const [query, setQuery] = useState("");
|
||
const { mutateAsync: linkCase, isPending } = useLinkRelatedCase(caseId);
|
||
const alreadyLinkedIds = new Set([...currentRelated.map((r) => r.id), caseId]);
|
||
|
||
const { data, isPending: searching } = usePrecedents(
|
||
query.length >= 2 ? { search: query, limit: 10 } : {},
|
||
);
|
||
|
||
const candidates = (data?.items ?? []).filter((p) => !alreadyLinkedIds.has(p.id));
|
||
|
||
async function handleLink(relatedId: string) {
|
||
try {
|
||
await linkCase({ relatedId });
|
||
toast.success("הפסיקות קושרו");
|
||
setQuery("");
|
||
} catch {
|
||
toast.error("שגיאה בקישור");
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="max-w-lg" dir="rtl">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-navy">קשר החלטה קשורה</DialogTitle>
|
||
<DialogDescription className="sr-only">חיפוש וקישור תקדים או החלטה קשורה לתיק</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-3">
|
||
<Input
|
||
placeholder="חפש לפי מספר תיק, שם, ערכאה..."
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
autoFocus
|
||
dir="rtl"
|
||
/>
|
||
|
||
{query.length >= 2 && (
|
||
<div className="space-y-1 max-h-72 overflow-y-auto">
|
||
{searching ? (
|
||
<div className="flex items-center gap-2 text-ink-muted text-sm py-3">
|
||
<Loader2 className="w-3.5 h-3.5 animate-spin" /> מחפש...
|
||
</div>
|
||
) : candidates.length === 0 ? (
|
||
<p className="text-ink-muted text-sm py-3 text-center">לא נמצאו תוצאות</p>
|
||
) : (
|
||
candidates.map((p) => (
|
||
<button
|
||
key={p.id}
|
||
onClick={() => handleLink(p.id)}
|
||
disabled={isPending}
|
||
className="w-full text-right flex items-center justify-between gap-3 px-3 py-2.5 rounded-lg border border-rule hover:bg-surface/60 transition-colors disabled:opacity-50"
|
||
>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="text-sm font-medium text-navy truncate">
|
||
{p.case_name || p.case_number}
|
||
</div>
|
||
<div className="text-[0.72rem] text-ink-muted font-mono" dir="ltr">
|
||
{p.case_number}
|
||
</div>
|
||
</div>
|
||
{p.precedent_level && (
|
||
<Badge
|
||
variant="outline"
|
||
className={`text-[0.62rem] shrink-0 ${LEVEL_COLORS[p.precedent_level] ?? ""}`}
|
||
>
|
||
{LEVEL_LABELS[p.precedent_level] ?? p.precedent_level}
|
||
</Badge>
|
||
)}
|
||
</button>
|
||
))
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{query.length > 0 && query.length < 2 && (
|
||
<p className="text-ink-muted text-xs">הקלד לפחות 2 תווים</p>
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|
||
|
||
// ── Public section component ─────────────────────────────────────────
|
||
|
||
type SectionProps = {
|
||
caseId: string;
|
||
related: RelatedCase[];
|
||
};
|
||
|
||
/* Rail-styled citations card (mockup 08 side rail). Renders linked related
|
||
* decisions as a navy-headed card with arrow-prefixed rows; keeps the full
|
||
* link/unlink logic. Used in the precedent-detail side rail. */
|
||
export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
|
||
return (
|
||
<div className="rounded-lg border border-rule bg-surface shadow-sm px-4 py-3.5 space-y-2.5">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<h3 className="text-navy text-[0.92rem] font-semibold m-0">
|
||
ציטוטים מקושרים{related.length > 0 ? ` (${related.length})` : ""}
|
||
</h3>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="h-7 px-2 text-[0.72rem] border-rule"
|
||
onClick={() => setDialogOpen(true)}
|
||
>
|
||
<Link2 className="w-3 h-3 me-1" /> קשר
|
||
</Button>
|
||
</div>
|
||
|
||
{related.length === 0 ? (
|
||
<p className="text-ink-muted text-[0.82rem] m-0">אין החלטות קשורות עדיין</p>
|
||
) : (
|
||
<ul className="list-none p-0 m-0">
|
||
{related.map((r) => (
|
||
<li
|
||
key={r.id}
|
||
className="flex items-start gap-2 py-2 border-b border-rule-soft last:border-b-0"
|
||
>
|
||
<span className="text-gold font-bold leading-6 shrink-0" aria-hidden>←</span>
|
||
<a
|
||
href={`/precedents/${r.id}`}
|
||
className="min-w-0 flex-1 group hover:opacity-90 transition-opacity"
|
||
>
|
||
<div className="text-[0.82rem] text-ink-soft leading-5 group-hover:text-navy">
|
||
{r.case_name || r.case_number}
|
||
</div>
|
||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||
{r.precedent_level && (
|
||
<Badge
|
||
variant="outline"
|
||
className={`text-[0.6rem] ${LEVEL_COLORS[r.precedent_level] ?? ""}`}
|
||
>
|
||
{LEVEL_LABELS[r.precedent_level] ?? r.precedent_level}
|
||
</Badge>
|
||
)}
|
||
{r.court && (
|
||
<span className="text-[0.68rem] text-ink-muted truncate">{r.court}</span>
|
||
)}
|
||
{r.date && (
|
||
<span className="text-[0.68rem] text-ink-muted tabular-nums" dir="ltr">
|
||
{r.date.slice(0, 10)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</a>
|
||
<UnlinkButton caseId={caseId} relatedId={r.id} />
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
|
||
<LinkDialog
|
||
caseId={caseId}
|
||
currentRelated={related}
|
||
open={dialogOpen}
|
||
onOpenChange={setDialogOpen}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function UnlinkButton({ caseId, relatedId }: { caseId: string; relatedId: string }) {
|
||
const { mutateAsync: unlinkCase, isPending } = useUnlinkRelatedCase(caseId);
|
||
return (
|
||
<button
|
||
onClick={async () => {
|
||
try {
|
||
await unlinkCase(relatedId);
|
||
toast.success("הקישור הוסר");
|
||
} catch {
|
||
toast.error("שגיאה בהסרת הקישור");
|
||
}
|
||
}}
|
||
disabled={isPending}
|
||
className="p-0.5 rounded hover:bg-danger-bg hover:text-danger transition-colors text-ink-muted disabled:opacity-40 shrink-0"
|
||
title="הסר קישור"
|
||
>
|
||
<X className="w-3 h-3" />
|
||
</button>
|
||
);
|
||
}
|