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>
259 lines
9.9 KiB
TypeScript
259 lines
9.9 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Trash2, Upload, Pencil, ExternalLink } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
import Link from "next/link";
|
||
import {
|
||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||
} from "@/components/ui/table";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import {
|
||
useMissingPrecedents,
|
||
useDeleteMissingPrecedent,
|
||
CITED_BY_PARTY_LABELS,
|
||
STATUS_LABELS,
|
||
type CitedByParty,
|
||
type MissingPrecedent,
|
||
type MissingPrecedentStatus,
|
||
} from "@/lib/api/missing-precedents";
|
||
import { MissingPrecedentDetailDrawer } from "./missing-precedent-detail-drawer";
|
||
|
||
function formatDate(iso: string | null) {
|
||
if (!iso) return "—";
|
||
try {
|
||
return new Date(iso).toLocaleDateString("he-IL");
|
||
} catch {
|
||
return iso;
|
||
}
|
||
}
|
||
|
||
/** Status chip — mockup 09 tones (open=warn, uploaded=info, closed=success,
|
||
* irrelevant=muted). Pill-shaped, whitespace-nowrap. */
|
||
function StatusBadge({ status }: { status: MissingPrecedentStatus }) {
|
||
const variants: Record<MissingPrecedentStatus, string> = {
|
||
open: "bg-warn-bg text-warn border-transparent",
|
||
uploaded: "bg-info-bg text-info border-transparent",
|
||
closed: "bg-success-bg text-success border-transparent",
|
||
irrelevant: "bg-rule-soft text-ink-muted border-transparent",
|
||
};
|
||
return (
|
||
<Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[status]}`}>
|
||
{STATUS_LABELS[status]}
|
||
</Badge>
|
||
);
|
||
}
|
||
|
||
/** Citing-party chip — colored by side (mockup 09 source chips). */
|
||
function SourceChip({ party }: { party: CitedByParty | null }) {
|
||
if (!party) return <span className="text-ink-muted text-sm">—</span>;
|
||
const variants: Record<CitedByParty, string> = {
|
||
appellant: "bg-info-bg text-info border-transparent",
|
||
respondent: "bg-gold-wash text-gold-deep border-rule",
|
||
committee: "bg-success-bg text-success border-transparent",
|
||
permit_applicant: "bg-info-bg text-info border-transparent",
|
||
unknown: "bg-rule-soft text-ink-muted border-transparent",
|
||
};
|
||
return (
|
||
<Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[party]}`}>
|
||
{CITED_BY_PARTY_LABELS[party]}
|
||
</Badge>
|
||
);
|
||
}
|
||
|
||
function TableSkeleton({ cols }: { cols: number }) {
|
||
return (
|
||
<>
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<TableRow key={i} className="border-rule">
|
||
{Array.from({ length: cols }).map((__, j) => (
|
||
<TableCell key={j}>
|
||
<Skeleton className="h-4 w-full" />
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))}
|
||
</>
|
||
);
|
||
}
|
||
|
||
type Props = {
|
||
status?: MissingPrecedentStatus | "";
|
||
caseNumber?: string;
|
||
legalTopic?: string;
|
||
};
|
||
|
||
export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props) {
|
||
const [openId, setOpenId] = useState<string | null>(null);
|
||
const { data, isPending, error } = useMissingPrecedents({
|
||
status: status === "" ? undefined : status,
|
||
caseNumber,
|
||
legalTopic,
|
||
limit: 200,
|
||
});
|
||
const del = useDeleteMissingPrecedent();
|
||
|
||
const handleDelete = async (mp: MissingPrecedent) => {
|
||
if (!confirm(`למחוק את הרשומה? ${mp.case_name || mp.citation.slice(0, 60)}...`)) {
|
||
return;
|
||
}
|
||
try {
|
||
await del.mutateAsync(mp.id);
|
||
toast.success("הרשומה נמחקה");
|
||
} catch (e) {
|
||
toast.error("מחיקה נכשלה");
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
||
{error.message}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||
<Table>
|
||
<TableHeader className="bg-parchment">
|
||
<TableRow className="border-rule hover:bg-transparent">
|
||
<TableHead className="text-ink-muted text-right font-medium text-xs">פסיקה</TableHead>
|
||
<TableHead className="text-ink-muted text-right font-medium text-xs">נושא</TableHead>
|
||
<TableHead className="text-ink-muted text-right font-medium text-xs">תיק</TableHead>
|
||
<TableHead className="text-ink-muted text-right font-medium text-xs">צוטט ע״י</TableHead>
|
||
<TableHead className="text-ink-muted text-right font-medium text-xs">סטטוס</TableHead>
|
||
<TableHead className="text-ink-muted text-right font-medium text-xs">נוצר</TableHead>
|
||
<TableHead className="text-navy" />
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{isPending ? (
|
||
<TableSkeleton cols={7} />
|
||
) : !data?.items.length ? (
|
||
<TableRow className="border-rule">
|
||
<TableCell colSpan={7} className="text-center text-ink-muted py-8">
|
||
אין פסיקות חסרות בקריטריונים הנוכחיים.
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
data.items.map((mp) => (
|
||
<TableRow
|
||
key={mp.id}
|
||
className="border-rule hover:bg-rule-soft/30 cursor-pointer"
|
||
onClick={() => setOpenId(mp.id)}
|
||
>
|
||
<TableCell className="max-w-[440px]">
|
||
<div className="text-sm text-navy font-semibold truncate">
|
||
{mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")}
|
||
</div>
|
||
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
||
{mp.citation}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className="text-sm text-ink">{mp.legal_topic || "—"}</span>
|
||
</TableCell>
|
||
<TableCell>
|
||
{mp.cited_in_case_number ? (
|
||
<Link
|
||
href={`/cases/${encodeURIComponent(mp.cited_in_case_number)}`}
|
||
onClick={(e) => e.stopPropagation()}
|
||
className="text-sm text-navy hover:text-gold-deep inline-flex items-center gap-1"
|
||
>
|
||
{mp.cited_in_case_number}
|
||
<ExternalLink className="w-3 h-3" />
|
||
</Link>
|
||
) : (
|
||
<span className="text-ink-muted text-sm">—</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell className="text-sm text-ink">
|
||
<SourceChip party={mp.cited_by_party} />
|
||
{mp.cited_by_party_name ? (
|
||
<div className="text-[0.7rem] text-ink-muted truncate max-w-[160px] mt-1">
|
||
{mp.cited_by_party_name}
|
||
</div>
|
||
) : null}
|
||
</TableCell>
|
||
<TableCell>
|
||
<StatusBadge status={mp.status} />
|
||
{mp.linked_case_law_number ? (
|
||
<div className="text-[0.7rem] text-success mt-1">
|
||
↳ {mp.linked_case_law_name || mp.linked_case_law_number}
|
||
</div>
|
||
) : null}
|
||
</TableCell>
|
||
<TableCell className="text-[0.78rem] text-ink-muted">
|
||
{formatDate(mp.created_at)}
|
||
</TableCell>
|
||
<TableCell className="text-end">
|
||
<div className="flex items-center justify-end gap-2">
|
||
{mp.status === "open" ? (
|
||
/* gold "העלה והשלם" CTA (mockup 09 `.btn`) */
|
||
<Button
|
||
size="sm"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setOpenId(mp.id);
|
||
}}
|
||
className="h-7 bg-gold text-white hover:bg-gold-deep border-transparent text-[0.78rem] font-semibold"
|
||
>
|
||
<Upload className="w-3.5 h-3.5 me-1" />
|
||
העלה והשלם
|
||
</Button>
|
||
) : (
|
||
/* passive "done" label for non-open rows */
|
||
<span className="inline-flex items-center gap-1 rounded-md bg-rule-soft text-ink-muted text-[0.78rem] font-medium px-2.5 py-1">
|
||
{mp.status === "closed" ? "קושר" : STATUS_LABELS[mp.status]} ✓
|
||
</span>
|
||
)}
|
||
{mp.status !== "open" ? (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setOpenId(mp.id);
|
||
}}
|
||
title="פרטים"
|
||
>
|
||
<Pencil className="w-4 h-4" />
|
||
</Button>
|
||
) : null}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleDelete(mp);
|
||
}}
|
||
disabled={del.isPending}
|
||
className="text-danger hover:text-danger"
|
||
title="מחיקה"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
|
||
<MissingPrecedentDetailDrawer
|
||
id={openId}
|
||
onOpenChange={(open) => {
|
||
if (!open) setOpenId(null);
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|