Merge pull request 'feat(ui): IA redesign → production · יישום נאמן של כל הדפים למוקאפים' (#213) from worktree-ia-redesign-faithful into main
This commit was merged in pull request #213.
This commit is contained in:
@@ -17,13 +17,6 @@ import {
|
|||||||
* פסיקה חסרה, הערות שטרם יושמו, ותיקים שנכשלו ב-QA. המטרה:
|
* פסיקה חסרה, הערות שטרם יושמו, ותיקים שנכשלו ב-QA. המטרה:
|
||||||
* שאף פריט הדורש את אישורך לא יישכח. הנתונים נשלפים חי מ-/api/chair/pending.
|
* שאף פריט הדורש את אישורך לא יישכח. הנתונים נשלפים חי מ-/api/chair/pending.
|
||||||
*/
|
*/
|
||||||
const SEVERITY_BADGE: Record<ApprovalSeverity, string> = {
|
|
||||||
high: "bg-gold text-navy border-transparent",
|
|
||||||
medium: "bg-gold-wash text-gold-deep border-gold/40",
|
|
||||||
low: "bg-rule-soft text-ink-muted border-rule",
|
|
||||||
ok: "bg-emerald-50 text-emerald-800 border-emerald-300/60",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Severity expressed as a colored dot next to the title (matches the approved
|
// Severity expressed as a colored dot next to the title (matches the approved
|
||||||
// IA-redesign mockup): high=danger, medium=warn, low=info, ok=success.
|
// IA-redesign mockup): high=danger, medium=warn, low=info, ok=success.
|
||||||
const SEVERITY_DOT: Record<ApprovalSeverity, string> = {
|
const SEVERITY_DOT: Record<ApprovalSeverity, string> = {
|
||||||
@@ -49,67 +42,84 @@ function formatDate(iso?: string | null): string {
|
|||||||
function ApprovalCard({ cat }: { cat: ApprovalCategory }) {
|
function ApprovalCard({ cat }: { cat: ApprovalCategory }) {
|
||||||
const cleared = cat.count === 0;
|
const cleared = cat.count === 0;
|
||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm flex flex-col">
|
<Card className="bg-surface border-rule shadow-sm flex flex-col overflow-hidden">
|
||||||
<CardContent className="px-6 py-5 flex flex-col gap-3 grow">
|
<CardContent className="p-0 flex flex-col grow">
|
||||||
<div className="flex items-start gap-3">
|
{/* top row — severity dot · title+age · big count number (mockup 01) */}
|
||||||
|
<div className="flex items-start gap-3 px-5 pt-5 pb-2">
|
||||||
<span
|
<span
|
||||||
className={`mt-2 h-2.5 w-2.5 shrink-0 rounded-full ${SEVERITY_DOT[cat.severity]}`}
|
className={`mt-2 h-2.5 w-2.5 shrink-0 rounded-full ${SEVERITY_DOT[cat.severity]}`}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<h2 className="text-navy text-lg mb-0 leading-snug grow">{cat.label}</h2>
|
<div className="grow min-w-0">
|
||||||
|
<h2 className="text-navy text-base font-semibold mb-0 leading-snug">{cat.label}</h2>
|
||||||
|
<div className="text-[0.78rem] text-ink-muted mt-0.5">
|
||||||
|
{cleared ? (
|
||||||
|
<span className="inline-block rounded-full bg-success-bg text-success text-[0.72rem] px-2.5 py-0.5 font-medium">
|
||||||
|
תור נקי
|
||||||
|
</span>
|
||||||
|
) : cat.oldest_at ? (
|
||||||
|
<>הוותיק ביותר — {formatDate(cat.oldest_at)}</>
|
||||||
|
) : (
|
||||||
|
cat.description
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center justify-center min-w-[2.25rem] h-7 px-2 rounded-full border text-sm font-semibold tabular-nums ${SEVERITY_BADGE[cat.severity]}`}
|
className="text-3xl font-bold text-navy leading-none tabular-nums shrink-0"
|
||||||
aria-label={`${cat.count} פריטים ממתינים`}
|
aria-label={`${cat.count} פריטים ממתינים`}
|
||||||
>
|
>
|
||||||
{cat.count}
|
{cat.count}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-ink-muted text-[0.85rem] leading-relaxed mb-0">
|
{/* description kept (subtle) when the age line took the title slot */}
|
||||||
{cat.description}
|
{!cleared && cat.oldest_at ? (
|
||||||
</p>
|
<p className="px-5 text-[0.8rem] text-ink-muted leading-relaxed mb-0">{cat.description}</p>
|
||||||
|
|
||||||
{cat.oldest_at && cat.count > 0 ? (
|
|
||||||
<p className="text-[0.78rem] text-gold-deep mb-0">
|
|
||||||
הישן ביותר ממתין מ־{formatDate(cat.oldest_at)}
|
|
||||||
</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{cat.extra ? (
|
{cat.extra ? (
|
||||||
<p className="text-[0.78rem] text-ink-muted mb-0">
|
<p className="px-5 mt-1 text-[0.78rem] text-ink-muted mb-0">
|
||||||
סך {cat.extra.total} שאילתות · {cat.extra.reviewed} אושרו על־ידך
|
סך {cat.extra.total} שאילתות · {cat.extra.reviewed} אושרו על־ידך
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{cleared ? (
|
{!cleared && cat.sample && cat.sample.length > 0 ? (
|
||||||
<p className="text-[0.85rem] text-emerald-700 mb-0">אין פריטים ממתינים ✓</p>
|
<ul className="mt-3 px-5 border-t border-rule-soft">
|
||||||
) : cat.sample && cat.sample.length > 0 ? (
|
|
||||||
<ul className="space-y-1.5 mt-1">
|
|
||||||
{cat.sample.map((s, i) => {
|
{cat.sample.map((s, i) => {
|
||||||
const body = (
|
const row = (
|
||||||
<>
|
<div className="flex items-start gap-2">
|
||||||
<span className="line-clamp-2">{s.text || "—"}</span>
|
<span className="line-clamp-2 text-ink-soft">{s.text || "—"}</span>
|
||||||
{s.source ? (
|
{s.source ? (
|
||||||
<span className="text-ink-muted text-[0.72rem]"> · {s.source}</span>
|
<span className="text-ink-muted text-[0.72rem] ms-auto shrink-0 whitespace-nowrap">
|
||||||
|
{s.source}
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<li key={i} className="text-[0.82rem] text-ink leading-snug border-s-2 border-rule ps-2.5">
|
<li
|
||||||
|
key={i}
|
||||||
|
className="text-[0.82rem] py-2.5 border-b border-rule-soft last:border-b-0"
|
||||||
|
>
|
||||||
{s.href ? (
|
{s.href ? (
|
||||||
<Link href={s.href} className="hover:text-gold-deep hover:underline block">
|
<Link href={s.href} className="block hover:text-gold-deep">
|
||||||
{body}
|
{row}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
body
|
row
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
) : cleared ? (
|
||||||
|
<p className="px-5 mt-2 text-[0.85rem] text-ink-muted mb-0">
|
||||||
|
אין פריטים הממתינים להתייחסות. כל התיקים עברו בדיקת-איכות.
|
||||||
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-auto pt-2">
|
{/* foot — gold CTA when actionable, quiet outline when cleared */}
|
||||||
|
<div className="mt-auto px-5 pt-4 pb-5">
|
||||||
{cat.href ? (
|
{cat.href ? (
|
||||||
cleared ? (
|
cleared ? (
|
||||||
<Button asChild variant="outline" size="sm" className="border-rule text-ink-muted">
|
<Button asChild variant="outline" size="sm" className="border-rule text-ink-muted">
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useCases, useRestoreCase, type Case } from "@/lib/api/cases";
|
import { useCases, useRestoreCase, type Case } from "@/lib/api/cases";
|
||||||
import { APPEAL_SUBTYPE_LABELS } from "@/lib/practice-area";
|
import { subtypeOf } from "@/components/cases/appeal-type-bars";
|
||||||
|
import { APPEAL_SUBTYPE_LABELS, type AppealSubtype } from "@/lib/practice-area";
|
||||||
|
|
||||||
function formatDate(iso?: string | null) {
|
function formatDate(iso?: string | null) {
|
||||||
if (!iso) return "—";
|
if (!iso) return "—";
|
||||||
@@ -41,6 +42,20 @@ function formatDate(iso?: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type chip styling per mockup 05 (.t-lic / .t-bet / .t-comp)
|
||||||
|
const TYPE_CHIP: Record<string, string> = {
|
||||||
|
building_permit: "bg-info-bg text-info",
|
||||||
|
betterment_levy: "bg-gold-wash text-gold-deep border border-rule",
|
||||||
|
compensation_197: "bg-rule-soft text-ink-soft",
|
||||||
|
};
|
||||||
|
|
||||||
|
// outcome chip styling per mockup 05 (.r-acc / .r-rej / .r-part)
|
||||||
|
const OUTCOME_CHIP: Record<string, { label: string; cls: string }> = {
|
||||||
|
full_acceptance: { label: "התקבל", cls: "bg-success-bg text-success" },
|
||||||
|
partial_acceptance: { label: "חלקי", cls: "bg-warn-bg text-warn" },
|
||||||
|
rejection: { label: "נדחה", cls: "bg-danger-bg text-danger" },
|
||||||
|
};
|
||||||
|
|
||||||
function RestoreButton({ caseNumber }: { caseNumber: string }) {
|
function RestoreButton({ caseNumber }: { caseNumber: string }) {
|
||||||
const restore = useRestoreCase(caseNumber);
|
const restore = useRestoreCase(caseNumber);
|
||||||
return (
|
return (
|
||||||
@@ -77,7 +92,7 @@ function RestoreButton({ caseNumber }: { caseNumber: string }) {
|
|||||||
const columns: ColumnDef<Case>[] = [
|
const columns: ColumnDef<Case>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "case_number",
|
accessorKey: "case_number",
|
||||||
header: "מס׳ ערר",
|
header: "מספר ערר",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Link
|
<Link
|
||||||
href={`/cases/${row.original.case_number}`}
|
href={`/cases/${row.original.case_number}`}
|
||||||
@@ -91,20 +106,42 @@ const columns: ColumnDef<Case>[] = [
|
|||||||
accessorKey: "title",
|
accessorKey: "title",
|
||||||
header: "כותרת",
|
header: "כותרת",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-ink max-w-[420px] truncate" title={row.original.title}>
|
<div className="text-ink-soft max-w-[420px] truncate" title={row.original.title}>
|
||||||
{row.original.title}
|
{row.original.title}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "appeal_subtype",
|
accessorKey: "appeal_subtype",
|
||||||
header: "תחום",
|
header: "סוג",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original.appeal_subtype;
|
const s = subtypeOf(row.original);
|
||||||
if (!s || s === "unknown")
|
if (!s || s === "unknown")
|
||||||
return <span className="text-ink-muted">—</span>;
|
return <span className="text-ink-muted">—</span>;
|
||||||
return (
|
return (
|
||||||
<span className="text-ink-soft text-sm">{APPEAL_SUBTYPE_LABELS[s]}</span>
|
<span
|
||||||
|
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${
|
||||||
|
TYPE_CHIP[s] ?? "bg-rule-soft text-ink-soft"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{APPEAL_SUBTYPE_LABELS[s as AppealSubtype]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "expected_outcome",
|
||||||
|
header: "תוצאה",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const o = row.original.expected_outcome;
|
||||||
|
const chip = o ? OUTCOME_CHIP[o] : undefined;
|
||||||
|
if (!chip) return <span className="text-ink-muted">—</span>;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${chip.cls}`}
|
||||||
|
>
|
||||||
|
{chip.label}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -112,7 +149,7 @@ const columns: ColumnDef<Case>[] = [
|
|||||||
accessorKey: "archived_at",
|
accessorKey: "archived_at",
|
||||||
header: "תאריך ארכוב",
|
header: "תאריך ארכוב",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="text-ink-muted text-sm tabular-nums">
|
<span className="text-ink-muted text-sm tabular-nums whitespace-nowrap">
|
||||||
{formatDate(row.original.archived_at)}
|
{formatDate(row.original.archived_at)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -130,6 +167,7 @@ export default function ArchivePage() {
|
|||||||
{ id: "archived_at", desc: true },
|
{ id: "archived_at", desc: true },
|
||||||
]);
|
]);
|
||||||
const [globalFilter, setGlobalFilter] = useState("");
|
const [globalFilter, setGlobalFilter] = useState("");
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>("all");
|
||||||
|
|
||||||
const rows = useMemo(() => data ?? [], [data]);
|
const rows = useMemo(() => data ?? [], [data]);
|
||||||
|
|
||||||
@@ -152,126 +190,141 @@ export default function ArchivePage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// domain filter applied client-side (subtypeOf collapses בל"מ variants)
|
||||||
|
const filteredRows = useMemo(() => {
|
||||||
|
const all = table.getFilteredRowModel().rows;
|
||||||
|
if (typeFilter === "all") return all;
|
||||||
|
return all.filter((r) => subtypeOf(r.original) === typeFilter);
|
||||||
|
}, [table, typeFilter, globalFilter, sorting, rows]);
|
||||||
|
|
||||||
|
const total = rows.length;
|
||||||
|
const shown = filteredRows.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-8">
|
<section className="space-y-6">
|
||||||
<header className="space-y-1.5">
|
<header className="space-y-1.5">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<span aria-hidden> · </span>
|
<span aria-hidden> · </span>
|
||||||
<span className="text-navy">ארכיון</span>
|
<span className="text-navy">ארכיון</span>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="flex items-end justify-between gap-4 flex-wrap">
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
||||||
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
ארכיון תיקי ערר
|
||||||
ארכיון תיקי ערר
|
|
||||||
</div>
|
|
||||||
<h1 className="text-navy mb-0">תיקים סגורים</h1>
|
|
||||||
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
|
|
||||||
תיקים שסגרו את הטיפול בהם. שחזור מחזיר את התיק לרשימה הראשית
|
|
||||||
ופותח מחדש את הפרויקט המקביל ב-Paperclip.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5">
|
|
||||||
<span className="text-2xl font-semibold text-gold-deep leading-none tabular-nums">
|
|
||||||
{table.getFilteredRowModel().rows.length}
|
|
||||||
</span>
|
|
||||||
<span className="text-[0.85rem] text-ink-soft">תיקים בארכיון</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="text-navy mb-0">ארכיון</h1>
|
||||||
|
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
|
||||||
|
כל ההחלטות שהושלמו — לחיפוש, סינון ועיון. {total} תיקים סגורים.
|
||||||
|
שחזור מחזיר את התיק לרשימה הראשית ופותח מחדש את הפרויקט המקביל ב-Paperclip.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
{/* filter bar — search + domain select + count aligned to end (mockup 05 .filters) */}
|
||||||
<CardContent className="px-6 py-5 space-y-4">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<div className="flex items-center gap-3">
|
<Input
|
||||||
<Input
|
value={globalFilter}
|
||||||
value={globalFilter}
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
placeholder="חיפוש לפי מספר ערר, כותרת או צד…"
|
||||||
placeholder="חיפוש לפי מס׳ ערר או כותרת…"
|
className="flex-1 min-w-[220px] bg-surface"
|
||||||
className="max-w-sm bg-surface"
|
dir="rtl"
|
||||||
dir="rtl"
|
/>
|
||||||
/>
|
<select
|
||||||
</div>
|
value={typeFilter}
|
||||||
|
onChange={(e) => setTypeFilter(e.target.value)}
|
||||||
|
className="cursor-pointer text-[0.84rem] text-ink-soft bg-surface border border-rule rounded-lg px-3.5 py-2"
|
||||||
|
>
|
||||||
|
<option value="all">כל סוגי הערר</option>
|
||||||
|
<option value="building_permit">רישוי ובנייה</option>
|
||||||
|
<option value="betterment_levy">היטל השבחה</option>
|
||||||
|
<option value="compensation_197">פיצויים (ס׳ 197)</option>
|
||||||
|
</select>
|
||||||
|
<span className="ms-auto text-[0.82rem] text-ink-muted tabular-nums">
|
||||||
|
מציג {shown} מתוך {total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
{/* clean bordered table — parchment header, gold-wash hover (mockup 05 .card table) */}
|
||||||
<Table>
|
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||||
<TableHeader className="bg-rule-soft/60">
|
<CardContent className="p-0">
|
||||||
{table.getHeaderGroups().map((hg) => (
|
<Table>
|
||||||
<TableRow key={hg.id} className="border-rule">
|
<TableHeader className="bg-parchment">
|
||||||
{hg.headers.map((header) => (
|
{table.getHeaderGroups().map((hg) => (
|
||||||
<TableHead
|
<TableRow key={hg.id} className="border-rule">
|
||||||
key={header.id}
|
{hg.headers.map((header) => (
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
<TableHead
|
||||||
className="text-navy font-semibold cursor-pointer select-none text-right"
|
key={header.id}
|
||||||
>
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
{flexRender(
|
className="text-ink-muted font-medium text-[0.78rem] cursor-pointer select-none text-start"
|
||||||
header.column.columnDef.header,
|
>
|
||||||
header.getContext(),
|
{flexRender(
|
||||||
)}
|
header.column.columnDef.header,
|
||||||
{{ asc: " ▲", desc: " ▼" }[
|
header.getContext(),
|
||||||
header.column.getIsSorted() as string
|
)}
|
||||||
] ?? ""}
|
{{ asc: " ▲", desc: " ▼" }[
|
||||||
</TableHead>
|
header.column.getIsSorted() as string
|
||||||
|
] ?? ""}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{isPending ? (
|
||||||
|
Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<TableRow key={i} className="border-rule-soft">
|
||||||
|
{columns.map((_c, j) => (
|
||||||
|
<TableCell key={j}>
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
</TableHeader>
|
) : error ? (
|
||||||
<TableBody>
|
<TableRow>
|
||||||
{isPending ? (
|
<TableCell
|
||||||
Array.from({ length: 3 }).map((_, i) => (
|
colSpan={columns.length}
|
||||||
<TableRow key={i} className="border-rule">
|
className="text-center text-danger py-8"
|
||||||
{columns.map((_c, j) => (
|
>
|
||||||
<TableCell key={j}>
|
שגיאה בטעינת ארכיון: {error.message}
|
||||||
<Skeleton className="h-4 w-24" />
|
</TableCell>
|
||||||
</TableCell>
|
</TableRow>
|
||||||
))}
|
) : filteredRows.length === 0 ? (
|
||||||
</TableRow>
|
<TableRow>
|
||||||
))
|
<TableCell
|
||||||
) : error ? (
|
colSpan={columns.length}
|
||||||
<TableRow>
|
className="text-center text-ink-muted py-12"
|
||||||
<TableCell
|
>
|
||||||
colSpan={columns.length}
|
<div className="text-gold text-2xl mb-2" aria-hidden>
|
||||||
className="text-center text-danger py-8"
|
❦
|
||||||
>
|
</div>
|
||||||
שגיאה בטעינת ארכיון: {error.message}
|
{globalFilter || typeFilter !== "all"
|
||||||
</TableCell>
|
? "אין תיקים תואמים לחיפוש"
|
||||||
|
: "אין תיקים בארכיון"}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredRows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
className="border-rule-soft hover:bg-gold-wash transition-colors"
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="py-3">
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : table.getRowModel().rows.length === 0 ? (
|
))
|
||||||
<TableRow>
|
)}
|
||||||
<TableCell
|
</TableBody>
|
||||||
colSpan={columns.length}
|
</Table>
|
||||||
className="text-center text-ink-muted py-12"
|
|
||||||
>
|
|
||||||
<div className="text-gold text-2xl mb-2" aria-hidden>
|
|
||||||
❦
|
|
||||||
</div>
|
|
||||||
{globalFilter
|
|
||||||
? "אין תיקים תואמים לחיפוש"
|
|
||||||
: "אין תיקים בארכיון"}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
table.getRowModel().rows.map((row) => (
|
|
||||||
<TableRow
|
|
||||||
key={row.id}
|
|
||||||
className="border-rule hover:bg-gold-wash/40 transition-colors"
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<TableCell key={cell.id} className="py-3">
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext(),
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { use, useRef, useState } from "react";
|
import { use, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { FileText } from "lucide-react";
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -9,9 +10,51 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { SubsectionCard } from "@/components/compose/subsection-card";
|
import { SubsectionCard } from "@/components/compose/subsection-card";
|
||||||
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { useCase } from "@/lib/api/cases";
|
import { useCase, type CaseStatus } from "@/lib/api/cases";
|
||||||
import { useResearchAnalysis } from "@/lib/api/research";
|
import { useResearchAnalysis } from "@/lib/api/research";
|
||||||
import { useCasePrecedents } from "@/lib/api/precedents";
|
import { useCasePrecedents } from "@/lib/api/precedents";
|
||||||
|
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
|
||||||
|
import { DOC_TYPE_LABELS, type DocType } from "@/lib/doc-types";
|
||||||
|
|
||||||
|
// ── Case-status → Hebrew label + tone (mockup 03 status chip) ────────────────
|
||||||
|
const STATUS_CHIP: Record<string, { label: string; cls: string }> = {
|
||||||
|
new: { label: "חדש", cls: "bg-rule-soft text-ink-muted border-rule" },
|
||||||
|
uploading: { label: "בהעלאה", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
processing: { label: "בעיבוד", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
documents_ready: { label: "מסמכים מוכנים", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
analyst_verified: { label: "אומת ע״י אנליסט", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
research_complete: { label: "מחקר הושלם", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
outcome_set: { label: "תוצאה נקבעה", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
brainstorming: { label: "סיעור-מוחות", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
direction_approved: { label: "כיוון אושר", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
analysis_enriched: { label: "ניתוח הועשר", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
ready_for_writing: { label: "מוכן לכתיבה", cls: "bg-gold-wash text-gold-deep border-gold/40" },
|
||||||
|
drafting: { label: "בעריכה", cls: "bg-info-bg text-info border-info/30" },
|
||||||
|
qa_review: { label: "בדיקת-איכות", cls: "bg-gold-wash text-gold-deep border-gold/40" },
|
||||||
|
drafted: { label: "טיוטה", cls: "bg-gold-wash text-gold-deep border-gold/40" },
|
||||||
|
exported: { label: "יוצא", cls: "bg-success-bg text-success border-success/40" },
|
||||||
|
reviewed: { label: "נסקר", cls: "bg-success-bg text-success border-success/40" },
|
||||||
|
final: { label: "סופי", cls: "bg-success-bg text-success border-success/40" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatusChip({ status }: { status?: CaseStatus }) {
|
||||||
|
const c = (status && STATUS_CHIP[status]) || {
|
||||||
|
label: "בעריכה",
|
||||||
|
cls: "bg-info-bg text-info border-info/30",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border ${c.cls}`}
|
||||||
|
>
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function subtypeLabel(subtype?: string | null): string | null {
|
||||||
|
if (!subtype) return null;
|
||||||
|
return APPEAL_SUBTYPES.find((s) => s.value === subtype)?.label ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function ProseSection({ title, content }: { title: string; content?: string }) {
|
function ProseSection({ title, content }: { title: string; content?: string }) {
|
||||||
if (!content?.trim()) return null;
|
if (!content?.trim()) return null;
|
||||||
@@ -25,7 +68,8 @@ function ProseSection({ title, content }: { title: string; content?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AnalysisActions({
|
// ── "השלמה והעברה" rail card — DOCX export, upload, download (all real) ──────
|
||||||
|
function FinishRail({
|
||||||
caseNumber,
|
caseNumber,
|
||||||
hasAnalysis,
|
hasAnalysis,
|
||||||
onUploaded,
|
onUploaded,
|
||||||
@@ -55,7 +99,7 @@ function AnalysisActions({
|
|||||||
}
|
}
|
||||||
setUploadMsg({
|
setUploadMsg({
|
||||||
ok: true,
|
ok: true,
|
||||||
text: `הקובץ הועלה בהצלחה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`,
|
text: `הקובץ הועלה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`,
|
||||||
});
|
});
|
||||||
onUploaded();
|
onUploaded();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -67,58 +111,79 @@ function AnalysisActions({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
{uploadMsg && (
|
<CardContent className="px-4 py-4">
|
||||||
<span className={`text-xs ${uploadMsg.ok ? "text-green-700" : "text-red-600"}`}>
|
<h3 className="text-navy text-[0.9rem] font-semibold mb-3">השלמה והעברה</h3>
|
||||||
{uploadMsg.text}
|
|
||||||
</span>
|
<input
|
||||||
)}
|
ref={fileRef}
|
||||||
<input
|
type="file"
|
||||||
ref={fileRef}
|
accept=".md"
|
||||||
type="file"
|
className="hidden"
|
||||||
accept=".md"
|
onChange={(e) => {
|
||||||
className="hidden"
|
const f = e.target.files?.[0];
|
||||||
onChange={(e) => {
|
if (f) handleUpload(f);
|
||||||
const f = e.target.files?.[0];
|
|
||||||
if (f) handleUpload(f);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
disabled={uploading}
|
|
||||||
onClick={() => fileRef.current?.click()}
|
|
||||||
>
|
|
||||||
{uploading ? "מעלה..." : "העלה ניתוח מעודכן"}
|
|
||||||
</Button>
|
|
||||||
{hasAnalysis && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = `/api/cases/${caseNumber}/research/analysis/download`;
|
|
||||||
a.download = `analysis-${caseNumber}.md`;
|
|
||||||
a.click();
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
הורד ניתוח
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{hasAnalysis && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-center"
|
||||||
|
onClick={() => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
|
||||||
|
a.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ייצוא DOCX
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className="w-full justify-center bg-gold text-white hover:bg-gold-deep"
|
||||||
|
disabled={uploading}
|
||||||
|
onClick={() => fileRef.current?.click()}
|
||||||
|
>
|
||||||
|
{uploading ? "מעלה…" : "העלאת ניתוח מעודכן"}
|
||||||
|
</Button>
|
||||||
|
{hasAnalysis && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-center"
|
||||||
|
onClick={() => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = `/api/cases/${caseNumber}/research/analysis/download`;
|
||||||
|
a.download = `analysis-${caseNumber}.md`;
|
||||||
|
a.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
הורד ניתוח (MD)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{uploadMsg && (
|
||||||
|
<p className={`text-xs mt-2 ${uploadMsg.ok ? "text-success" : "text-danger"}`}>
|
||||||
|
{uploadMsg.text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* mockup 03: stage indicators — informational pointers, not actions */}
|
||||||
|
<div className="mt-3 space-y-0">
|
||||||
|
<div className="text-[0.78rem] text-ink-muted pt-2 border-t border-rule-soft">
|
||||||
|
<b className="text-navy">הרץ למידת-קול</b> — ממתין להעלאת הסופי
|
||||||
|
</div>
|
||||||
|
<div className="text-[0.78rem] text-ink-muted pt-2 mt-2 border-t border-rule-soft">
|
||||||
|
<b className="text-navy">הרץ אימות-הלכות</b> — ממתין להעלאת הסופי
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button asChild variant="ghost" className="w-full justify-center mt-3 text-ink-muted">
|
||||||
|
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</CardContent>
|
||||||
{hasAnalysis && (
|
</Card>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
|
|
||||||
a.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
הורד כ-DOCX
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button asChild variant="outline">
|
|
||||||
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +210,18 @@ export default function ComposePage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const practiceArea = caseQuery.data?.practice_area ?? null;
|
const practiceArea = caseQuery.data?.practice_area ?? null;
|
||||||
|
const subtype = subtypeLabel(caseQuery.data?.appeal_subtype);
|
||||||
|
const parties = (() => {
|
||||||
|
const c = caseQuery.data;
|
||||||
|
if (!c) return null;
|
||||||
|
const app = c.appellants?.length ? c.appellants.join(", ") : null;
|
||||||
|
const resp = c.respondents?.length ? c.respondents.join(", ") : null;
|
||||||
|
const out: string[] = [];
|
||||||
|
if (app) out.push(`עוררים: ${app}`);
|
||||||
|
if (resp) out.push(`משיבה: ${resp}`);
|
||||||
|
return out.length ? out.join(" · ") : c.title || null;
|
||||||
|
})();
|
||||||
|
const documents = caseQuery.data?.documents ?? [];
|
||||||
|
|
||||||
const isNotFound =
|
const isNotFound =
|
||||||
analysis.error instanceof Error &&
|
analysis.error instanceof Error &&
|
||||||
@@ -152,121 +229,84 @@ export default function ComposePage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
{/* ── Case header band (mockup 03) — parchment strip, full-bleed to the
|
||||||
{/* Header strip */}
|
AppShell <main> edges (which pads px-10 py-10) ── */}
|
||||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
<div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-5">
|
||||||
<div>
|
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-2">
|
||||||
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-1">
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<span aria-hidden>·</span>
|
||||||
<span aria-hidden>·</span>
|
<Link href={`/cases/${caseNumber}`} className="hover:text-gold-deep">
|
||||||
<Link
|
ערר {caseNumber}
|
||||||
href={`/cases/${caseNumber}`}
|
</Link>
|
||||||
className="hover:text-gold-deep"
|
<span aria-hidden>·</span>
|
||||||
>
|
<span className="text-navy">עורך החלטה</span>
|
||||||
ערר {caseNumber}
|
</nav>
|
||||||
</Link>
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<span aria-hidden>·</span>
|
<h1 className="text-navy text-2xl font-bold mb-0">ערר {caseNumber}</h1>
|
||||||
<span className="text-navy">עורך החלטה</span>
|
<StatusChip status={caseQuery.data?.status} />
|
||||||
</nav>
|
{subtype && (
|
||||||
<h1 className="text-navy mb-0">ניתוח משפטי וכתיבת עמדה</h1>
|
<span className="rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border border-rule bg-gold-wash text-gold-deep">
|
||||||
{caseQuery.data?.title && (
|
{subtype}
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
</span>
|
||||||
{caseQuery.data.title}
|
)}
|
||||||
</p>
|
{/* INV-G10: source-of-truth pill — the blocks are the canonical text */}
|
||||||
)}
|
<span className="ms-auto rounded-lg text-[0.8rem] font-semibold px-3.5 py-1.5 border border-gold bg-gold-wash text-gold-deep">
|
||||||
</div>
|
מקור-אמת: בלוקים
|
||||||
<AnalysisActions caseNumber={caseNumber} hasAnalysis={!!analysis.data} onUploaded={() => analysis.refetch()} />
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{parties && <p className="text-ink-soft text-sm mt-2">{parties}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
{analysis.isPending ? (
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
{analysis.isPending ? (
|
<CardContent className="px-6 py-5 space-y-3">
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Skeleton className="h-6 w-48" />
|
||||||
<CardContent className="px-6 py-5 space-y-3">
|
<Skeleton className="h-4 w-96" />
|
||||||
<Skeleton className="h-6 w-48" />
|
<Skeleton className="h-4 w-80" />
|
||||||
<Skeleton className="h-4 w-96" />
|
<Skeleton className="h-32 w-full" />
|
||||||
<Skeleton className="h-4 w-80" />
|
</CardContent>
|
||||||
<Skeleton className="h-32 w-full" />
|
</Card>
|
||||||
</CardContent>
|
) : isNotFound ? (
|
||||||
</Card>
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
) : isNotFound ? (
|
<CardContent className="px-6 py-12 text-center space-y-3">
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<div className="text-gold text-3xl" aria-hidden>❦</div>
|
||||||
<CardContent className="px-6 py-12 text-center space-y-3">
|
<h2 className="text-navy text-lg mb-0">
|
||||||
<div className="text-gold text-3xl" aria-hidden>❦</div>
|
טרם בוצע ניתוח משפטי לתיק זה
|
||||||
<h2 className="text-navy text-lg mb-0">
|
</h2>
|
||||||
טרם בוצע ניתוח משפטי לתיק זה
|
<p className="text-ink-muted text-sm max-w-md mx-auto">
|
||||||
</h2>
|
לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי
|
||||||
<p className="text-ink-muted text-sm max-w-md mx-auto">
|
לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.
|
||||||
לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי
|
</p>
|
||||||
לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.
|
</CardContent>
|
||||||
</p>
|
</Card>
|
||||||
</CardContent>
|
) : analysis.error ? (
|
||||||
</Card>
|
<Card className="bg-danger-bg border-danger/40">
|
||||||
) : analysis.error ? (
|
<CardContent className="px-6 py-5 text-center">
|
||||||
<Card className="bg-danger-bg border-danger/40">
|
<p className="text-danger">{analysis.error.message}</p>
|
||||||
<CardContent className="px-6 py-5 text-center">
|
</CardContent>
|
||||||
<p className="text-danger">{analysis.error.message}</p>
|
</Card>
|
||||||
</CardContent>
|
) : analysis.data ? (
|
||||||
</Card>
|
/* ── Two-column workspace: main editor list + 320px side rail ──────── */
|
||||||
) : analysis.data ? (
|
<div className="grid gap-6 lg:grid-cols-[1fr_320px] items-start">
|
||||||
<div className="space-y-6">
|
{/* MAIN — the block/subsection editor list */}
|
||||||
{/* Case-level general precedents */}
|
<div className="space-y-6 min-w-0">
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
{/* Threshold claims */}
|
||||||
<CardContent className="px-6 py-5">
|
{analysis.data.threshold_claims &&
|
||||||
<h2 className="text-navy text-xl mb-1">פסיקה כללית לדיון</h2>
|
analysis.data.threshold_claims.length > 0 && (
|
||||||
<p className="text-[0.78rem] text-ink-muted mb-4">
|
|
||||||
ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון).
|
|
||||||
</p>
|
|
||||||
<PrecedentsSection
|
|
||||||
caseNumber={caseNumber}
|
|
||||||
sectionId={null}
|
|
||||||
precedents={caseLevelPrecedents}
|
|
||||||
practiceArea={practiceArea}
|
|
||||||
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Threshold claims */}
|
|
||||||
{analysis.data.threshold_claims &&
|
|
||||||
analysis.data.threshold_claims.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="text-navy text-xl mb-0">טענות סף</h2>
|
|
||||||
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
|
||||||
{analysis.data.threshold_claims.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{analysis.data.threshold_claims.map((tc) => (
|
|
||||||
<SubsectionCard
|
|
||||||
key={tc.id}
|
|
||||||
caseNumber={caseNumber}
|
|
||||||
item={tc}
|
|
||||||
precedents={precedentsBySection.get(tc.id) ?? []}
|
|
||||||
practiceArea={practiceArea}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Issues */}
|
|
||||||
{analysis.data.issues && analysis.data.issues.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-navy text-xl mb-0">סוגיות להכרעה</h2>
|
<h2 className="text-navy text-lg font-semibold mb-0">טענות סף</h2>
|
||||||
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
||||||
{analysis.data.issues.length}
|
{analysis.data.threshold_claims.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-2.5">
|
||||||
{analysis.data.issues.map((iss) => (
|
{analysis.data.threshold_claims.map((tc) => (
|
||||||
<SubsectionCard
|
<SubsectionCard
|
||||||
key={iss.id}
|
key={tc.id}
|
||||||
caseNumber={caseNumber}
|
caseNumber={caseNumber}
|
||||||
item={iss}
|
item={tc}
|
||||||
precedents={precedentsBySection.get(iss.id) ?? []}
|
precedents={precedentsBySection.get(tc.id) ?? []}
|
||||||
practiceArea={practiceArea}
|
practiceArea={practiceArea}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -274,8 +314,31 @@ export default function ComposePage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(!analysis.data.threshold_claims?.length &&
|
{/* Issues */}
|
||||||
!analysis.data.issues?.length) && (
|
{analysis.data.issues && analysis.data.issues.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-navy text-lg font-semibold mb-0">סוגיות להכרעה</h2>
|
||||||
|
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
||||||
|
{analysis.data.issues.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{analysis.data.issues.map((iss) => (
|
||||||
|
<SubsectionCard
|
||||||
|
key={iss.id}
|
||||||
|
caseNumber={caseNumber}
|
||||||
|
item={iss}
|
||||||
|
precedents={precedentsBySection.get(iss.id) ?? []}
|
||||||
|
practiceArea={practiceArea}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!analysis.data.threshold_claims?.length &&
|
||||||
|
!analysis.data.issues?.length && (
|
||||||
<Card className="bg-surface border-rule">
|
<Card className="bg-surface border-rule">
|
||||||
<CardContent className="px-6 py-10 text-center text-ink-muted">
|
<CardContent className="px-6 py-10 text-center text-ink-muted">
|
||||||
לא נמצאו טענות סף או סוגיות בניתוח זה.
|
לא נמצאו טענות סף או סוגיות בניתוח זה.
|
||||||
@@ -283,42 +346,82 @@ export default function ComposePage({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Background prose — moved below the issues so it reads as
|
{/* Background prose — supporting context after the decision points */}
|
||||||
supporting context after the chair has seen the main
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
decision points, not as a wall of text beside them. */}
|
<CardContent className="px-6 py-5 space-y-5">
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<h2 className="text-navy text-lg font-semibold mb-0">רקע לניתוח</h2>
|
||||||
<CardContent className="px-6 py-5 space-y-5">
|
<ProseSection title="צד מיוצג" content={analysis.data.represented_party} />
|
||||||
<h2 className="text-navy text-xl mb-0">רקע לניתוח</h2>
|
<ProseSection title="רקע דיוני" content={analysis.data.procedural_background} />
|
||||||
<ProseSection
|
<ProseSection title="עובדות מוסכמות" content={analysis.data.agreed_facts} />
|
||||||
title="צד מיוצג"
|
<ProseSection title="עובדות במחלוקת" content={analysis.data.disputed_facts} />
|
||||||
content={analysis.data.represented_party}
|
</CardContent>
|
||||||
/>
|
</Card>
|
||||||
<ProseSection
|
|
||||||
title="רקע דיוני"
|
{analysis.data.conclusions?.trim() && (
|
||||||
content={analysis.data.procedural_background}
|
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||||||
/>
|
<CardContent className="px-6 py-5 space-y-3">
|
||||||
<ProseSection
|
<h2 className="text-gold-deep text-lg font-semibold mb-0">מסקנות</h2>
|
||||||
title="עובדות מוסכמות"
|
<Markdown content={analysis.data.conclusions.trim()} />
|
||||||
content={analysis.data.agreed_facts}
|
|
||||||
/>
|
|
||||||
<ProseSection
|
|
||||||
title="עובדות במחלוקת"
|
|
||||||
content={analysis.data.disputed_facts}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
{analysis.data.conclusions?.trim() && (
|
|
||||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
|
||||||
<CardContent className="px-6 py-5 space-y-3">
|
|
||||||
<h2 className="text-gold-deep text-xl mb-0">מסקנות</h2>
|
|
||||||
<Markdown content={analysis.data.conclusions.trim()} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</section>
|
{/* SIDE RAIL — documents · attached precedents · finish-and-transfer */}
|
||||||
|
<aside className="space-y-4 lg:sticky lg:top-4">
|
||||||
|
{/* מסמכי התיק */}
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-4 py-4">
|
||||||
|
<h3 className="text-navy text-[0.9rem] font-semibold mb-2">מסמכי התיק</h3>
|
||||||
|
{documents.length === 0 ? (
|
||||||
|
<p className="text-[0.78rem] text-ink-muted">אין מסמכים מצורפים</p>
|
||||||
|
) : (
|
||||||
|
<ul>
|
||||||
|
{documents.map((d) => (
|
||||||
|
<li
|
||||||
|
key={d.id}
|
||||||
|
className="flex items-center gap-2 text-[0.82rem] text-ink-soft py-1.5 border-b border-rule-soft last:border-0"
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5 text-ink-muted shrink-0" aria-hidden />
|
||||||
|
<span className="truncate flex-1" title={d.title}>
|
||||||
|
{d.title || "מסמך"}
|
||||||
|
</span>
|
||||||
|
<span className="rounded bg-rule-soft text-ink-muted text-[0.68rem] px-1.5 py-0.5 shrink-0 whitespace-nowrap">
|
||||||
|
{DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* פסיקה מצורפת (case-level) */}
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-4 py-4">
|
||||||
|
<h3 className="text-navy text-[0.9rem] font-semibold mb-1">פסיקה מצורפת</h3>
|
||||||
|
<p className="text-[0.72rem] text-ink-muted mb-3">
|
||||||
|
ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון).
|
||||||
|
</p>
|
||||||
|
<PrecedentsSection
|
||||||
|
caseNumber={caseNumber}
|
||||||
|
sectionId={null}
|
||||||
|
precedents={caseLevelPrecedents}
|
||||||
|
practiceArea={practiceArea}
|
||||||
|
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* השלמה והעברה */}
|
||||||
|
<FinishRail
|
||||||
|
caseNumber={caseNumber}
|
||||||
|
hasAnalysis={!!analysis.data}
|
||||||
|
onUploaded={() => analysis.refetch()}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ export default function CaseDetailPage({
|
|||||||
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
|
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
if (error) {
|
||||||
<AppShell>
|
return (
|
||||||
<section className="space-y-6">
|
<AppShell>
|
||||||
{error ? (
|
<section className="space-y-6">
|
||||||
<Card className="bg-danger-bg border-danger/40">
|
<Card className="bg-danger-bg border-danger/40">
|
||||||
<CardContent className="px-6 py-6 text-center space-y-3">
|
<CardContent className="px-6 py-6 text-center space-y-3">
|
||||||
<p className="text-danger font-semibold">שגיאה בטעינת התיק</p>
|
<p className="text-danger font-semibold">שגיאה בטעינת התיק</p>
|
||||||
@@ -58,130 +58,168 @@ export default function CaseDetailPage({
|
|||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</section>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabsList = (
|
||||||
|
<TabsList
|
||||||
|
variant="line"
|
||||||
|
className="gap-6 h-auto p-0 rounded-none -mb-px"
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
["overview", "סקירה"],
|
||||||
|
["arguments", "טיעונים"],
|
||||||
|
["decision", "ההחלטה"],
|
||||||
|
["drafts", "טיוטות והערות"],
|
||||||
|
["agents", "סוכנים"],
|
||||||
|
].map(([value, label]) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
className="flex-none rounded-none px-0 pb-3.5 pt-0 text-[0.92rem] font-medium text-ink-muted data-active:text-navy data-active:font-semibold data-active:after:bg-gold data-active:after:bottom-0"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
);
|
||||||
|
|
||||||
|
const bandActions = (
|
||||||
|
<>
|
||||||
|
{data && <CaseEditDialog data={data} />}
|
||||||
|
<UploadSheet caseNumber={caseNumber} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell>
|
||||||
|
<Tabs defaultValue="overview" dir="rtl">
|
||||||
|
{/* parchment band — header (title/chips/parties/actions) + tab strip */}
|
||||||
|
{isPending ? (
|
||||||
|
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6 pb-4 space-y-3">
|
||||||
|
<Skeleton className="h-4 w-40" />
|
||||||
|
<Skeleton className="h-8 w-64" />
|
||||||
|
<Skeleton className="h-6 w-96" />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<CaseHeader data={data} actions={bandActions} tabs={tabsList} />
|
||||||
{isPending ? (
|
)}
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-6 py-5 space-y-3">
|
{/* two-column wrap — main tab content (1fr) + rail (340px) */}
|
||||||
<Skeleton className="h-4 w-40" />
|
<div className="grid gap-6 lg:grid-cols-[1fr_340px] items-start mt-6">
|
||||||
<Skeleton className="h-8 w-64" />
|
<div className="min-w-0">
|
||||||
<Skeleton className="h-6 w-96" />
|
<TabsContent value="overview" className="mt-0 space-y-5">
|
||||||
|
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||||
|
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
|
||||||
|
סקירת התיק
|
||||||
|
</div>
|
||||||
|
<CardContent className="px-5 py-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-navy text-[0.95rem] font-semibold mb-1.5">תוצאה צפויה</h3>
|
||||||
|
<p className="text-ink-soft text-sm leading-relaxed">
|
||||||
|
{expectedOutcomeLabel ?? "לא נקבעה תוצאה צפויה."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t border-rule-soft">
|
||||||
|
<dl className="grid grid-cols-2 gap-y-2 gap-x-6 text-sm">
|
||||||
|
<dt className="text-ink-muted">בעיבוד</dt>
|
||||||
|
<dd className="text-ink tabular-nums">
|
||||||
|
{data?.processing_count ?? 0}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
{canStartWorkflow && (
|
||||||
|
<div className="pt-2 border-t border-rule-soft">
|
||||||
|
<Button
|
||||||
|
className="bg-gold-deep hover:bg-gold-deep/90 text-parchment"
|
||||||
|
disabled={startWorkflow.isPending}
|
||||||
|
onClick={() =>
|
||||||
|
startWorkflow.mutate(undefined, {
|
||||||
|
onSuccess: (res) =>
|
||||||
|
toast.success(
|
||||||
|
`תהליך הופעל — ${res.issue_identifier}`,
|
||||||
|
),
|
||||||
|
onError: (err) =>
|
||||||
|
toast.error(`שגיאה: ${err.message}`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{startWorkflow.isPending ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin me-1.5" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4 me-1.5" />
|
||||||
|
)}
|
||||||
|
התחל תהליך
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
|
||||||
<CaseHeader data={data} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-[1fr_280px]">
|
<DocumentsPanel data={data} />
|
||||||
|
|
||||||
|
{/* gold CTA — open the decision editor (mockup .cta) */}
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
className="w-full bg-gold text-white hover:bg-gold-deep border-transparent py-6 text-base font-semibold"
|
||||||
|
>
|
||||||
|
<Link href={`/cases/${caseNumber}/compose`}>
|
||||||
|
פתח עורך החלטה →
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="arguments" className="mt-0">
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
<Tabs defaultValue="overview" dir="rtl">
|
<LegalArgumentsPanel caseNumber={caseNumber} />
|
||||||
<div className="flex items-center justify-between gap-3 mb-1 flex-wrap">
|
|
||||||
<TabsList className="bg-rule-soft/60">
|
|
||||||
<TabsTrigger value="overview">סקירה</TabsTrigger>
|
|
||||||
<TabsTrigger value="arguments">
|
|
||||||
טיעונים
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="decision">
|
|
||||||
ההחלטה
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="drafts">
|
|
||||||
טיוטות והערות
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="agents">
|
|
||||||
סוכנים
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
|
||||||
<Link href={`/cases/${caseNumber}/compose`}>
|
|
||||||
פתח בעורך ההחלטה
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
{data && <CaseEditDialog data={data} />}
|
|
||||||
<UploadSheet caseNumber={caseNumber} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TabsContent value="overview" className="mt-5 space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-navy text-base mb-2">תוצאה צפויה</h3>
|
|
||||||
<p className="text-ink-soft text-sm leading-relaxed">
|
|
||||||
{expectedOutcomeLabel ?? "לא נקבעה תוצאה צפויה."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-navy text-base mb-2">סיכום מהיר</h3>
|
|
||||||
<dl className="grid grid-cols-2 gap-y-2 gap-x-6 text-sm">
|
|
||||||
<dt className="text-ink-muted">בעיבוד</dt>
|
|
||||||
<dd className="text-ink tabular-nums">
|
|
||||||
{data?.processing_count ?? 0}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
{canStartWorkflow && (
|
|
||||||
<div className="pt-2 border-t border-rule">
|
|
||||||
<Button
|
|
||||||
className="bg-gold-deep hover:bg-gold-deep/90 text-parchment"
|
|
||||||
disabled={startWorkflow.isPending}
|
|
||||||
onClick={() =>
|
|
||||||
startWorkflow.mutate(undefined, {
|
|
||||||
onSuccess: (res) =>
|
|
||||||
toast.success(
|
|
||||||
`תהליך הופעל — ${res.issue_identifier}`,
|
|
||||||
),
|
|
||||||
onError: (err) =>
|
|
||||||
toast.error(`שגיאה: ${err.message}`),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{startWorkflow.isPending ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin me-1.5" />
|
|
||||||
) : (
|
|
||||||
<Play className="w-4 h-4 me-1.5" />
|
|
||||||
)}
|
|
||||||
התחל תהליך
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DocumentsPanel data={data} />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="arguments" className="mt-5">
|
|
||||||
<LegalArgumentsPanel caseNumber={caseNumber} />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="decision" className="mt-5">
|
|
||||||
<DecisionBlocksPanel caseNumber={caseNumber} />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="drafts" className="mt-5">
|
|
||||||
<DraftsPanel
|
|
||||||
caseNumber={caseNumber}
|
|
||||||
status={data?.status}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="agents" className="mt-5">
|
|
||||||
<AgentActivityFeed caseNumber={caseNumber} />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm h-fit">
|
<TabsContent value="decision" className="mt-0">
|
||||||
<CardContent className="px-6 py-5 space-y-5">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<AgentStatusWidget caseNumber={caseNumber} />
|
<CardContent className="px-6 py-5">
|
||||||
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2>
|
<DecisionBlocksPanel caseNumber={caseNumber} />
|
||||||
<WorkflowTimeline status={data?.status} />
|
|
||||||
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
|
|
||||||
<StatusGuide />
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</TabsContent>
|
||||||
</>
|
|
||||||
)}
|
<TabsContent value="drafts" className="mt-0">
|
||||||
</section>
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5">
|
||||||
|
<DraftsPanel caseNumber={caseNumber} status={data?.status} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="agents" className="mt-0">
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5">
|
||||||
|
<AgentActivityFeed caseNumber={caseNumber} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* rail — status timeline + status controls (mockup .rail) */}
|
||||||
|
<div className="space-y-5">
|
||||||
|
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0 h-fit">
|
||||||
|
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
|
||||||
|
סטטוס התיק
|
||||||
|
</div>
|
||||||
|
<CardContent className="px-5 py-4 space-y-4">
|
||||||
|
<AgentStatusWidget caseNumber={caseNumber} />
|
||||||
|
<WorkflowTimeline status={data?.status} />
|
||||||
|
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
|
||||||
|
<StatusGuide />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { DigestListPanel } from "@/components/digests/digest-list-panel";
|
import { DigestListPanel } from "@/components/digests/digest-list-panel";
|
||||||
import { DigestSearchPanel } from "@/components/digests/digest-search-panel";
|
import { DigestSearchPanel } from "@/components/digests/digest-search-panel";
|
||||||
|
import { DigestUploadDialog } from "@/components/digests/digest-upload-dialog";
|
||||||
import { useDigestPending } from "@/lib/api/digests";
|
import { useDigestPending } from "@/lib/api/digests";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,44 +38,68 @@ export default function DigestsPage() {
|
|||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<header>
|
<header className="space-y-2">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<nav className="text-[0.78rem] text-ink-muted">
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<span aria-hidden> · </span>
|
<span aria-hidden> · </span>
|
||||||
<span className="text-navy">יומונים</span>
|
<span className="text-navy">יומונים</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 className="text-navy mb-0">יומונים — רדאר פסיקה</h1>
|
<h1 className="text-navy mb-0">יומונים (רדאר)</h1>
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl">
|
<p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
|
||||||
סיכומי "כל יום" (עפר טויסטר) של פסקי דין והחלטות עדכניים.
|
שכבת-גילוי משנית — מצביע-לא-מצוטט (X12). מאתרת פסיקה רלוונטית ומפנה
|
||||||
שכבת-גילוי בלבד: כל יומון <strong>מצביע</strong> על פסק הדין המקורי —
|
אליה; אינה מקור-אמת לציטוט. סיכומי "כל יום" (עפר טויסטר):
|
||||||
הוא אינו מצוטט בהחלטה ואינו מחלץ הלכות. כשהפסק רלוונטי, מעלים אותו
|
כל יומון <strong>מצביע</strong> על פסק הדין המקורי — כשהפסק רלוונטי,
|
||||||
לספריית הפסיקה ומצטטים משם.
|
מעלים אותו לספריית הפסיקה ומצטטים משם.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
{/* prominent dashed-gold upload area (mockup 10 `.upload`) */}
|
||||||
|
<div className="flex items-center gap-4 rounded-lg border-[1.5px] border-dashed border-gold bg-surface px-5 py-4">
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gold-wash text-gold-deep text-xl">
|
||||||
|
↑
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="block text-navy font-semibold text-[0.92rem]">העלאת יומון</span>
|
||||||
|
<span className="text-[0.78rem] text-ink-muted">
|
||||||
|
בחר קובץ יומון "כל יום" — PDF · עד 20MB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ms-auto">
|
||||||
|
<DigestUploadDialog
|
||||||
|
trigger={
|
||||||
|
<button className="rounded-lg bg-gold px-4 py-2 text-sm font-semibold text-white hover:bg-gold-deep">
|
||||||
|
בחר קובץ
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Tabs defaultValue="list" dir="rtl">
|
||||||
<CardContent className="px-6 py-5">
|
<TabsList className="flex w-full justify-start gap-1 rounded-none border-0 border-b border-rule bg-transparent p-0 h-auto">
|
||||||
<Tabs defaultValue="list" dir="rtl">
|
{[
|
||||||
<TabsList className="bg-rule-soft/60">
|
{ value: "list", label: "יומונים", pill: <PendingBadge /> },
|
||||||
<TabsTrigger value="list">
|
{ value: "search", label: "חיפוש", pill: null },
|
||||||
יומונים
|
].map((t) => (
|
||||||
<PendingBadge />
|
<TabsTrigger
|
||||||
</TabsTrigger>
|
key={t.value}
|
||||||
<TabsTrigger value="search">חיפוש</TabsTrigger>
|
value={t.value}
|
||||||
</TabsList>
|
className="rounded-none border-0 border-b-2 border-transparent bg-transparent px-4 py-2.5 -mb-px text-sm font-medium text-ink-muted shadow-none data-[state=active]:border-gold data-[state=active]:bg-transparent data-[state=active]:font-semibold data-[state=active]:text-navy data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
{t.pill}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="list" className="mt-5">
|
<TabsContent value="list" className="mt-5">
|
||||||
<DigestListPanel />
|
<DigestListPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="search" className="mt-5">
|
<TabsContent value="search" className="mt-5">
|
||||||
<DigestSearchPanel />
|
<DigestSearchPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import { toast } from "sonner";
|
|||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
useFeedbackList,
|
useFeedbackList,
|
||||||
useResolveFeedback,
|
useResolveFeedback,
|
||||||
|
useCreateFeedback,
|
||||||
CATEGORY_LABELS,
|
CATEGORY_LABELS,
|
||||||
CATEGORY_COLORS,
|
|
||||||
BLOCK_LABELS,
|
BLOCK_LABELS,
|
||||||
type ChairFeedback,
|
type ChairFeedback,
|
||||||
type FeedbackCategory,
|
type FeedbackCategory,
|
||||||
@@ -24,6 +23,16 @@ import {
|
|||||||
* "טרם יושמו" וקטגוריה, וסימון כל הערה כיושמה. מוזן מ-/api/feedback.
|
* "טרם יושמו" וקטגוריה, וסימון כל הערה כיושמה. מוזן מ-/api/feedback.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// category chip styling per mockup 06 (.c-missing / .c-tone / .c-struct / .c-fact / .c-style)
|
||||||
|
const CAT_CHIP: Record<FeedbackCategory, string> = {
|
||||||
|
missing_content: "bg-warn-bg text-warn",
|
||||||
|
wrong_tone: "bg-info-bg text-info",
|
||||||
|
wrong_structure: "bg-gold-wash text-gold-deep border border-rule",
|
||||||
|
factual_error: "bg-danger-bg text-danger",
|
||||||
|
style: "bg-rule-soft text-ink-soft",
|
||||||
|
other: "bg-rule-soft text-ink-soft",
|
||||||
|
};
|
||||||
|
|
||||||
function formatDate(iso?: string | null): string {
|
function formatDate(iso?: string | null): string {
|
||||||
if (!iso) return "";
|
if (!iso) return "";
|
||||||
try {
|
try {
|
||||||
@@ -57,47 +66,51 @@ function FeedbackCard({ fb }: { fb: ChairFeedback }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}>
|
<Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-[0.78]" : ""}`}>
|
||||||
<CardContent className="px-5 py-4 space-y-2.5">
|
<CardContent className="px-[18px] py-4 space-y-2.5">
|
||||||
<div className="flex items-start gap-2 flex-wrap">
|
{/* meta row — where · category chip · when (mockup 06 .meta) */}
|
||||||
<Badge variant="outline" className={`text-[0.7rem] ${CATEGORY_COLORS[fb.category]}`}>
|
<div className="flex items-center gap-2.5 flex-wrap">
|
||||||
|
<span className="font-semibold text-navy text-[0.84rem]">
|
||||||
|
{fb.case_number ? (
|
||||||
|
<Link
|
||||||
|
href={`/cases/${encodeURIComponent(fb.case_number)}`}
|
||||||
|
className="hover:text-gold-deep"
|
||||||
|
>
|
||||||
|
ערר {fb.case_number}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
"ללא תיק"
|
||||||
|
)}
|
||||||
|
<span className="text-ink-muted font-normal"> · {BLOCK_LABELS[fb.block_id] ?? fb.block_id}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${CAT_CHIP[fb.category]}`}
|
||||||
|
>
|
||||||
{CATEGORY_LABELS[fb.category]}
|
{CATEGORY_LABELS[fb.category]}
|
||||||
</Badge>
|
</span>
|
||||||
<Badge variant="outline" className="text-[0.7rem]">
|
<span className="ms-auto text-[0.72rem] text-ink-muted whitespace-nowrap">
|
||||||
{BLOCK_LABELS[fb.block_id] ?? fb.block_id}
|
|
||||||
</Badge>
|
|
||||||
{fb.case_number ? (
|
|
||||||
<Link
|
|
||||||
href={`/cases/${encodeURIComponent(fb.case_number)}`}
|
|
||||||
className="text-[0.72rem] text-gold-deep hover:underline"
|
|
||||||
>
|
|
||||||
תיק {fb.case_number}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span className="text-[0.72rem] text-ink-muted">ללא תיק</span>
|
|
||||||
)}
|
|
||||||
<span className="ms-auto text-[0.72rem] text-ink-muted">
|
|
||||||
{formatDate(fb.created_at)}
|
{formatDate(fb.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-navy text-sm leading-relaxed m-0 whitespace-pre-wrap" dir="rtl">
|
<p className="text-ink-soft text-sm leading-relaxed m-0 whitespace-pre-wrap" dir="rtl">
|
||||||
{fb.feedback_text}
|
{fb.feedback_text}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{fb.lesson_extracted ? (
|
{fb.lesson_extracted ? (
|
||||||
<div className="rounded-md bg-gold-wash/40 border-s-[3px] border-gold ps-3 pe-3 py-2">
|
<p
|
||||||
<div className="text-[0.68rem] text-gold-deep mb-0.5">לקח שהופק</div>
|
className="text-[0.82rem] text-ink-muted italic leading-relaxed m-0 whitespace-pre-wrap border-s-[3px] border-gold ps-2.5"
|
||||||
<p className="text-ink-soft text-[0.82rem] leading-relaxed m-0 whitespace-pre-wrap italic" dir="rtl">
|
dir="rtl"
|
||||||
{fb.lesson_extracted}
|
>
|
||||||
</p>
|
לקח שחולץ: {fb.lesson_extracted}
|
||||||
</div>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex items-center justify-end pt-1 border-t border-rule-soft">
|
{/* action row — gold CTA / applied pill (mockup 06 .actrow) */}
|
||||||
|
<div className="flex items-center justify-start pt-1">
|
||||||
{fb.resolved ? (
|
{fb.resolved ? (
|
||||||
<span className="flex items-center gap-1 text-[0.78rem] text-emerald-700">
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-success-bg text-success text-[0.76rem] font-semibold px-3 py-1">
|
||||||
<CheckCircle2 className="w-3.5 h-3.5" /> יושמה
|
<CheckCircle2 className="w-3.5 h-3.5" /> יושם
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -118,6 +131,118 @@ function FeedbackCard({ fb }: { fb: ChairFeedback }) {
|
|||||||
|
|
||||||
type CatFilter = "all" | FeedbackCategory;
|
type CatFilter = "all" | FeedbackCategory;
|
||||||
|
|
||||||
|
const BLOCK_OPTIONS = [
|
||||||
|
"block-vav",
|
||||||
|
"block-zayin",
|
||||||
|
"block-chet",
|
||||||
|
"block-tet",
|
||||||
|
"block-yod",
|
||||||
|
"block-yod-alef",
|
||||||
|
];
|
||||||
|
|
||||||
|
function AddFeedbackForm() {
|
||||||
|
const create = useCreateFeedback();
|
||||||
|
const [caseNumber, setCaseNumber] = useState("");
|
||||||
|
const [blockId, setBlockId] = useState("block-yod");
|
||||||
|
const [category, setCategory] = useState<FeedbackCategory>("missing_content");
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
|
||||||
|
const onSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!text.trim()) {
|
||||||
|
toast.error("יש להזין את תוכן ההערה");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
create.mutate(
|
||||||
|
{
|
||||||
|
case_number: caseNumber.trim() || undefined,
|
||||||
|
block_id: blockId,
|
||||||
|
category,
|
||||||
|
feedback_text: text.trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("ההערה נשמרה");
|
||||||
|
setCaseNumber("");
|
||||||
|
setText("");
|
||||||
|
},
|
||||||
|
onError: (err) =>
|
||||||
|
toast.error(err instanceof Error ? err.message : "שגיאה בשמירה"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldCls =
|
||||||
|
"w-full text-[0.84rem] text-ink bg-parchment border border-rule rounded-md px-3 py-2";
|
||||||
|
const labelCls = "block text-[0.76rem] text-ink-muted font-medium mt-2.5 mb-1";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-surface border-rule shadow-sm lg:sticky lg:top-6">
|
||||||
|
<CardContent className="px-5 py-4">
|
||||||
|
<h3 className="text-navy text-base font-semibold mb-3.5">הוסף הערה</h3>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<label className={labelCls} htmlFor="fb-case">מספר ערר</label>
|
||||||
|
<input
|
||||||
|
id="fb-case"
|
||||||
|
type="text"
|
||||||
|
value={caseNumber}
|
||||||
|
onChange={(e) => setCaseNumber(e.target.value)}
|
||||||
|
placeholder="לדוגמה: 1126-08-25"
|
||||||
|
className={fieldCls}
|
||||||
|
dir="rtl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className={labelCls} htmlFor="fb-block">בלוק</label>
|
||||||
|
<select
|
||||||
|
id="fb-block"
|
||||||
|
value={blockId}
|
||||||
|
onChange={(e) => setBlockId(e.target.value)}
|
||||||
|
className={`${fieldCls} cursor-pointer`}
|
||||||
|
>
|
||||||
|
{BLOCK_OPTIONS.map((b) => (
|
||||||
|
<option key={b} value={b}>
|
||||||
|
{BLOCK_LABELS[b]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label className={labelCls} htmlFor="fb-cat">קטגוריה</label>
|
||||||
|
<select
|
||||||
|
id="fb-cat"
|
||||||
|
value={category}
|
||||||
|
onChange={(e) => setCategory(e.target.value as FeedbackCategory)}
|
||||||
|
className={`${fieldCls} cursor-pointer`}
|
||||||
|
>
|
||||||
|
{(Object.keys(CATEGORY_LABELS) as FeedbackCategory[]).map((c) => (
|
||||||
|
<option key={c} value={c}>
|
||||||
|
{CATEGORY_LABELS[c]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label className={labelCls} htmlFor="fb-text">תוכן ההערה</label>
|
||||||
|
<textarea
|
||||||
|
id="fb-text"
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
placeholder="מה צריך לתקן ולמה…"
|
||||||
|
className={`${fieldCls} min-h-[90px] resize-y leading-relaxed`}
|
||||||
|
dir="rtl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={create.isPending}
|
||||||
|
className="w-full mt-4 bg-gold text-white hover:bg-gold-deep border-transparent"
|
||||||
|
>
|
||||||
|
{create.isPending ? "שומר…" : "שמור הערה"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function FeedbackPage() {
|
export default function FeedbackPage() {
|
||||||
const [unresolvedOnly, setUnresolvedOnly] = useState(true);
|
const [unresolvedOnly, setUnresolvedOnly] = useState(true);
|
||||||
const [category, setCategory] = useState<CatFilter>("all");
|
const [category, setCategory] = useState<CatFilter>("all");
|
||||||
@@ -159,14 +284,19 @@ export default function FeedbackPage() {
|
|||||||
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
||||||
שערים אנושיים · יו״ר הוועדה
|
שערים אנושיים · יו״ר הוועדה
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-navy mb-0">הערות יו״ר</h1>
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<h1 className="text-navy mb-0">הערות יו״ר</h1>
|
||||||
|
<span className="inline-block rounded-full bg-gold-wash border border-rule text-ink-muted text-[0.76rem] px-3 py-0.5">
|
||||||
|
נגיש גם מתוך מרכז האישורים
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
|
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
|
||||||
כל ההערות שנרשמו על טיוטות — מכל התיקים. סמן כל הערה כיושמה
|
כל ההערות שנרשמו על טיוטות — מכל התיקים. סמן כל הערה כיושמה
|
||||||
לאחר שהלקח הוטמע.
|
לאחר שהלקח הוטמע.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-end">
|
<div className="text-end">
|
||||||
<div className="text-3xl font-semibold text-navy leading-none">
|
<div className="text-3xl font-semibold text-navy leading-none tabular-nums">
|
||||||
{unresolvedCount}
|
{unresolvedCount}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted mt-1">
|
<div className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted mt-1">
|
||||||
@@ -178,15 +308,35 @@ export default function FeedbackPage() {
|
|||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|
||||||
{/* Filters */}
|
{/* category filter chips (mockup 06 .chips) + resolved-state toggle */}
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{categories.map((c) => {
|
||||||
|
const on = category === c.key;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={c.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCategory(c.key)}
|
||||||
|
className={`text-[0.8rem] font-medium px-3.5 py-1.5 rounded-full border transition-colors ${
|
||||||
|
on
|
||||||
|
? "bg-navy text-white border-navy"
|
||||||
|
: "bg-surface text-ink-soft border-rule hover:bg-rule-soft"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{c.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 ms-auto">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setUnresolvedOnly(true)}
|
onClick={() => setUnresolvedOnly(true)}
|
||||||
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
|
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
|
||||||
unresolvedOnly
|
unresolvedOnly
|
||||||
? "bg-navy text-parchment border-navy"
|
? "bg-navy text-white border-navy"
|
||||||
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
|
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -197,60 +347,50 @@ export default function FeedbackPage() {
|
|||||||
onClick={() => setUnresolvedOnly(false)}
|
onClick={() => setUnresolvedOnly(false)}
|
||||||
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
|
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
|
||||||
!unresolvedOnly
|
!unresolvedOnly
|
||||||
? "bg-navy text-parchment border-navy"
|
? "bg-navy text-white border-navy"
|
||||||
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
|
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
הכל
|
הכל
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 flex-wrap ms-auto">
|
|
||||||
{categories.map((c) => (
|
|
||||||
<button
|
|
||||||
key={c.key}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setCategory(c.key)}
|
|
||||||
className={`text-[0.74rem] px-2.5 py-1 rounded border transition-colors ${
|
|
||||||
category === c.key
|
|
||||||
? "bg-gold-wash text-gold-deep border-gold/40"
|
|
||||||
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{c.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{/* two-column body — feedback list + sticky add-form rail (mockup 06 .wrap grid) */}
|
||||||
<Card className="bg-surface border-rule">
|
<div className="grid gap-6 lg:grid-cols-[1fr_340px] items-start">
|
||||||
<CardContent className="px-6 py-5 text-ink-muted text-sm">
|
<div>
|
||||||
שגיאה בטעינת ההערות. נסה לרענן.
|
{error ? (
|
||||||
</CardContent>
|
<Card className="bg-surface border-rule">
|
||||||
</Card>
|
<CardContent className="px-6 py-5 text-ink-muted text-sm">
|
||||||
) : isPending ? (
|
שגיאה בטעינת ההערות. נסה לרענן.
|
||||||
<div className="space-y-3">
|
</CardContent>
|
||||||
{[0, 1, 2].map((i) => (
|
|
||||||
<Card key={i} className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-5 py-4 h-28 animate-pulse" />
|
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
) : isPending ? (
|
||||||
</div>
|
<div className="space-y-3.5">
|
||||||
) : items.length === 0 ? (
|
{[0, 1, 2].map((i) => (
|
||||||
<div className="text-center text-ink-muted py-16">
|
<Card key={i} className="bg-surface border-rule shadow-sm">
|
||||||
<p className="text-lg">אין הערות בקטגוריה זו.</p>
|
<CardContent className="px-[18px] py-4 h-28 animate-pulse" />
|
||||||
{unresolvedOnly && (
|
</Card>
|
||||||
<p className="text-sm mt-2">כל ההערות יושמו ✓</p>
|
))}
|
||||||
|
</div>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<div className="text-center text-ink-muted py-16">
|
||||||
|
<p className="text-lg">אין הערות בקטגוריה זו.</p>
|
||||||
|
{unresolvedOnly && (
|
||||||
|
<p className="text-sm mt-2">כל ההערות יושמו ✓</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3.5">
|
||||||
|
{items.map((fb) => (
|
||||||
|
<FeedbackCard key={fb.id} fb={fb} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
<AddFeedbackForm />
|
||||||
{items.map((fb) => (
|
</div>
|
||||||
<FeedbackCard key={fb.id} fb={fb} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function GraphPage() {
|
|||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<header>
|
<header className="space-y-1.5">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||||
<Link href="/" className="hover:text-gold-deep">
|
<Link href="/" className="hover:text-gold-deep">
|
||||||
בית
|
בית
|
||||||
@@ -16,16 +16,20 @@ export default function GraphPage() {
|
|||||||
<span aria-hidden> · </span>
|
<span aria-hidden> · </span>
|
||||||
<span className="text-navy">מפת הקורפוס</span>
|
<span className="text-navy">מפת הקורפוס</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
||||||
|
גרף הציטוטים · ספריית הפסיקה
|
||||||
|
</div>
|
||||||
<h1 className="text-navy mb-0">מפת הקורפוס</h1>
|
<h1 className="text-navy mb-0">מפת הקורפוס</h1>
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl">
|
<p className="text-ink-muted text-sm mt-1 max-w-3xl leading-relaxed">
|
||||||
רשת הציטוטים של ספריית הפסיקה — כל נקודה היא פסיקה או נושא, וקו מציין ציטוט או שיוך.
|
גרף הציטוטים של הקורפוס — פסיקה, נושאים והלכות וקשרי-ההפניה ביניהם.
|
||||||
גודל הנקודה משקף כמה פעמים הפסיקה צוטטה. לחצו על נקודה כדי להתמקד בשכניה.
|
גררו צמתים, סננו שכבות, ובחנו את מרכזי-הכובד. גודל הנקודה משקף כמה
|
||||||
|
פעמים הפסיקה צוטטה; לחיצה על נקודה ממקדת בשכניה.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="grid h-[560px] place-items-center text-sm text-ink-muted">
|
<div className="grid h-[560px] place-items-center rounded-lg border border-rule bg-gradient-to-b from-[#f3ecda] to-[#efe6cf] text-sm text-ink-muted">
|
||||||
טוען גרף…
|
טוען גרף…
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ export default function MethodologyPage() {
|
|||||||
</nav>
|
</nav>
|
||||||
<h1 className="text-navy mb-0">מתודולוגיה</h1>
|
<h1 className="text-navy mb-0">מתודולוגיה</h1>
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||||
הגדרות ניסוח — יחסי אורך, כללי דיון, וצ׳קליסטים לפי סוג ערר
|
העורך הקנוני היחיד לכללי-הכותב — יחסי אורך, כללי דיון, וצ׳קליסטים לפי סוג ערר
|
||||||
</p>
|
</p>
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-lg border border-success bg-success-bg text-success px-3.5 py-1.5 text-[0.78rem] font-semibold mt-3">
|
||||||
|
<span aria-hidden>●</span>
|
||||||
|
מקור-אמת יחיד — כל הסוכנים קוראים את הכללים מכאן בלבד
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
useMissingPrecedents,
|
useMissingPrecedents,
|
||||||
@@ -17,32 +14,26 @@ import { MissingPrecedentsTable } from "@/components/missing-precedents/missing-
|
|||||||
* Missing-precedents page (TaskMaster #35).
|
* Missing-precedents page (TaskMaster #35).
|
||||||
*
|
*
|
||||||
* Surfaces citations that party briefs invoke but which aren't yet in the
|
* Surfaces citations that party briefs invoke but which aren't yet in the
|
||||||
* precedent_library. Four tabs by status; each tab uses the same table
|
* precedent_library. A status filter (chips) narrows the table; each row uses
|
||||||
* component with a different filter. Drawer (sheet) opens on row click
|
* the same table component. Drawer (sheet) opens on row click with metadata +
|
||||||
* with metadata + upload form that routes to internal_decision_upload
|
* upload form that routes to internal_decision_upload (ערר/בל"מ citations) or
|
||||||
* (ערר/בל"מ citations) or precedent_library_upload (court rulings).
|
* precedent_library_upload (court rulings).
|
||||||
*/
|
*/
|
||||||
function StatusBadge({ status, count }: { status: MissingPrecedentStatus; count: number }) {
|
|
||||||
if (!count) return null;
|
type StatusFilter = MissingPrecedentStatus | "all";
|
||||||
const variants: Record<MissingPrecedentStatus, string> = {
|
|
||||||
open: "bg-gold-wash text-gold-deep border-gold/40",
|
const STATUS_CHIPS: { value: StatusFilter; label: string }[] = [
|
||||||
uploaded: "bg-rule-soft text-ink-muted border-rule",
|
{ value: "open", label: "פתוח" },
|
||||||
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60",
|
{ value: "uploaded", label: "הועלה" },
|
||||||
irrelevant: "bg-rule-soft text-ink-muted border-rule",
|
{ value: "closed", label: "נסגר" },
|
||||||
};
|
{ value: "irrelevant", label: "לא-רלוונטי" },
|
||||||
return (
|
{ value: "all", label: "הכל" },
|
||||||
<Badge
|
];
|
||||||
variant="outline"
|
|
||||||
className={`ms-1 text-[0.65rem] ${variants[status]}`}
|
|
||||||
>
|
|
||||||
{count}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MissingPrecedentsPage() {
|
export default function MissingPrecedentsPage() {
|
||||||
const [caseNumber, setCaseNumber] = useState("");
|
const [caseNumber, setCaseNumber] = useState("");
|
||||||
const [legalTopic, setLegalTopic] = useState("");
|
const [legalTopic, setLegalTopic] = useState("");
|
||||||
|
const [filter, setFilter] = useState<StatusFilter>("open");
|
||||||
|
|
||||||
const counts = useMissingPrecedents({ limit: 1 });
|
const counts = useMissingPrecedents({ limit: 1 });
|
||||||
const byStatus = counts.data?.by_status ?? {};
|
const byStatus = counts.data?.by_status ?? {};
|
||||||
@@ -50,124 +41,129 @@ export default function MissingPrecedentsPage() {
|
|||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<header>
|
<header className="space-y-3">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<nav className="text-[0.78rem] text-ink-muted">
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<span aria-hidden> · </span>
|
<span aria-hidden> · </span>
|
||||||
<span className="text-navy">פסיקה חסרה בקורפוס</span>
|
<span className="text-navy">פסיקה חסרה בקורפוס</span>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="flex items-end justify-between gap-4 flex-wrap">
|
|
||||||
<div>
|
{/* title + inline open-count pill (mockup 09 `.open-count`) */}
|
||||||
<h1 className="text-navy mb-0">פסיקה חסרה בקורפוס</h1>
|
<div className="flex items-baseline gap-3.5 flex-wrap">
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl">
|
<h1 className="text-navy mb-0">פסיקה חסרה בקורפוס</h1>
|
||||||
פסיקות שצוטטו בכתבי הטענות אך אינן עדיין בקורפוס. סוכן המחקר רושם
|
|
||||||
פערים אוטומטית; היו"ר סוגר אותם על־ידי העלאת המסמך — ניתוב
|
|
||||||
אוטומטי בין הקורפוס הסמכותי (פסקי דין) להחלטות ועדות ערר.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{byStatus.open ? (
|
{byStatus.open ? (
|
||||||
<div className="inline-flex items-baseline gap-2 rounded-lg border border-rule bg-warn-bg px-4 py-2.5">
|
<span className="inline-flex items-baseline gap-1.5 rounded-lg border border-rule bg-warn-bg px-3.5 py-1">
|
||||||
<span className="text-2xl font-semibold text-warn leading-none tabular-nums">
|
<span className="text-lg font-bold text-warn tabular-nums leading-none">
|
||||||
{byStatus.open}
|
{byStatus.open}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[0.85rem] text-ink-soft">פתוחים</span>
|
<span className="text-[0.8rem] text-ink-soft">פתוחים</span>
|
||||||
</div>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
|
||||||
|
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
|
||||||
|
אימות-הלכה ועיגון-מקור (INV-AH). סוכן המחקר רושם פערים אוטומטית;
|
||||||
|
היו"ר סוגר אותם על־ידי העלאת המסמך — ניתוב אוטומטי בין הקורפוס
|
||||||
|
הסמכותי (פסקי דין) להחלטות ועדות ערר.
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
{/* shared filters */}
|
||||||
<CardContent className="px-6 py-5 space-y-5">
|
<div className="flex items-end gap-3 flex-wrap">
|
||||||
{/* Shared filters */}
|
<div className="flex-1 min-w-[200px]">
|
||||||
<div className="flex items-end gap-3 flex-wrap">
|
<label className="block text-[0.78rem] text-ink-muted mb-1.5">תיק (מספר ערר)</label>
|
||||||
<div className="flex-1 min-w-[200px]">
|
<Input
|
||||||
<label className="text-[0.78rem] text-ink-muted">תיק (מספר ערר)</label>
|
value={caseNumber}
|
||||||
<Input
|
onChange={(e) => setCaseNumber(e.target.value)}
|
||||||
value={caseNumber}
|
placeholder="1017-03-26"
|
||||||
onChange={(e) => setCaseNumber(e.target.value)}
|
dir="rtl"
|
||||||
placeholder="1017-03-26"
|
/>
|
||||||
dir="rtl"
|
</div>
|
||||||
/>
|
<div className="flex-1 min-w-[200px]">
|
||||||
</div>
|
<label className="block text-[0.78rem] text-ink-muted mb-1.5">נושא משפטי</label>
|
||||||
<div className="flex-1 min-w-[200px]">
|
<Input
|
||||||
<label className="text-[0.78rem] text-ink-muted">נושא משפטי</label>
|
value={legalTopic}
|
||||||
<Input
|
onChange={(e) => setLegalTopic(e.target.value)}
|
||||||
value={legalTopic}
|
placeholder="זכות עמידה"
|
||||||
onChange={(e) => setLegalTopic(e.target.value)}
|
dir="rtl"
|
||||||
placeholder="זכות עמידה"
|
/>
|
||||||
dir="rtl"
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultValue="open" dir="rtl">
|
{/* status filter chips (mockup 09 `.filters`) — active = navy filled */}
|
||||||
<TabsList className="bg-rule-soft/60">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<TabsTrigger value="open">
|
{STATUS_CHIPS.map((c) => {
|
||||||
פתוחות
|
const active = filter === c.value;
|
||||||
<StatusBadge status="open" count={byStatus.open ?? 0} />
|
const count =
|
||||||
</TabsTrigger>
|
c.value === "all"
|
||||||
<TabsTrigger value="uploaded">
|
? undefined
|
||||||
הועלו
|
: (byStatus[c.value as MissingPrecedentStatus] ?? 0);
|
||||||
<StatusBadge status="uploaded" count={byStatus.uploaded ?? 0} />
|
return (
|
||||||
</TabsTrigger>
|
<button
|
||||||
<TabsTrigger value="closed">
|
key={c.value}
|
||||||
נסגרו
|
type="button"
|
||||||
<StatusBadge status="closed" count={byStatus.closed ?? 0} />
|
onClick={() => setFilter(c.value)}
|
||||||
</TabsTrigger>
|
aria-pressed={active}
|
||||||
<TabsTrigger value="irrelevant">
|
className={`rounded-full border px-4 py-1.5 text-[0.82rem] transition-colors ${
|
||||||
לא רלוונטי
|
active
|
||||||
<StatusBadge
|
? "bg-navy text-white border-navy font-semibold"
|
||||||
status="irrelevant"
|
: "bg-surface text-ink-soft border-rule font-medium hover:bg-rule-soft/50"
|
||||||
count={byStatus.irrelevant ?? 0}
|
}`}
|
||||||
/>
|
>
|
||||||
</TabsTrigger>
|
{c.label}
|
||||||
<TabsTrigger value="all">הכל</TabsTrigger>
|
{count ? (
|
||||||
</TabsList>
|
<span className="ms-1.5 tabular-nums opacity-80">({count})</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="open" className="mt-4">
|
<MissingPrecedentsTable
|
||||||
<MissingPrecedentsTable
|
status={filter === "all" ? "" : filter}
|
||||||
status="open"
|
caseNumber={caseNumber.trim() || undefined}
|
||||||
caseNumber={caseNumber.trim() || undefined}
|
legalTopic={legalTopic.trim() || undefined}
|
||||||
legalTopic={legalTopic.trim() || undefined}
|
/>
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="uploaded" className="mt-4">
|
{/* lifecycle note (mockup 09 `.lifecycle`) */}
|
||||||
<MissingPrecedentsTable
|
<div className="rounded-lg border border-rule bg-parchment px-5 py-3.5 text-[0.82rem] text-ink-muted leading-7">
|
||||||
status="uploaded"
|
<b className="text-ink-soft">מחזור-חיים:</b>{" "}
|
||||||
caseNumber={caseNumber.trim() || undefined}
|
<LifecycleChip tone="open">פתוח</LifecycleChip> →{" "}
|
||||||
legalTopic={legalTopic.trim() || undefined}
|
<LifecycleChip tone="up">הועלה</LifecycleChip> →{" "}
|
||||||
/>
|
<LifecycleChip tone="closed">נסגר</LifecycleChip>. פריט נפתח אוטומטית
|
||||||
</TabsContent>
|
בעת חילוץ ציטוט שאין לו תקדים בקורפוס; בהעלאת פסק-הדין הוא מקושר לרשומת
|
||||||
|
הפסיקה דרך{" "}
|
||||||
<TabsContent value="closed" className="mt-4">
|
<code className="rounded border border-rule bg-surface px-1.5 py-0.5 text-[0.75rem] text-gold-deep" dir="ltr">
|
||||||
<MissingPrecedentsTable
|
linked_case_law_id
|
||||||
status="closed"
|
</code>{" "}
|
||||||
caseNumber={caseNumber.trim() || undefined}
|
ונסגר. פריט שאינו רלוונטי מסומן{" "}
|
||||||
legalTopic={legalTopic.trim() || undefined}
|
<LifecycleChip tone="na">לא-רלוונטי</LifecycleChip> מבלי שתידרש העלאה.
|
||||||
/>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="irrelevant" className="mt-4">
|
|
||||||
<MissingPrecedentsTable
|
|
||||||
status="irrelevant"
|
|
||||||
caseNumber={caseNumber.trim() || undefined}
|
|
||||||
legalTopic={legalTopic.trim() || undefined}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="all" className="mt-4">
|
|
||||||
<MissingPrecedentsTable
|
|
||||||
caseNumber={caseNumber.trim() || undefined}
|
|
||||||
legalTopic={legalTopic.trim() || undefined}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LifecycleTone = "open" | "up" | "closed" | "na";
|
||||||
|
|
||||||
|
function LifecycleChip({
|
||||||
|
tone,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
tone: LifecycleTone;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const cls: Record<LifecycleTone, string> = {
|
||||||
|
open: "bg-warn-bg text-warn",
|
||||||
|
up: "bg-info-bg text-info",
|
||||||
|
closed: "bg-success-bg text-success",
|
||||||
|
na: "bg-rule-soft text-ink-muted",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold ${cls[tone]}`}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ function mb(bytes: number): string {
|
|||||||
return `${Math.round((bytes || 0) / 1024 / 1024)}MB`;
|
return `${Math.round((bytes || 0) / 1024 / 1024)}MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mockup 02: every region opens with a navy section heading (h2, 18px) — the
|
||||||
|
// page reads as a sequence of titled sections rather than a stack of cards.
|
||||||
|
function SectionHeader({ children }: { children: React.ReactNode }) {
|
||||||
|
return <h2 className="text-navy text-lg font-semibold mb-3 mt-2">{children}</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
function ago(ms: number): string {
|
function ago(ms: number): string {
|
||||||
if (!ms) return "—";
|
if (!ms) return "—";
|
||||||
const secs = Math.floor((Date.now() - ms) / 1000);
|
const secs = Math.floor((Date.now() - ms) / 1000);
|
||||||
@@ -189,64 +195,91 @@ function ServicesPanel({ data }: { data: OperationsSnapshot }) {
|
|||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
<h2 className="text-navy text-lg mb-1">ניהול תהליכי-רקע (pm2)</h2>
|
|
||||||
<p className="text-ink-muted text-xs mb-4">
|
<p className="text-ink-muted text-xs mb-4">
|
||||||
כמו "שירותים" ב-Windows. דמון = שירות רץ-תמיד (הפעל-מחדש/עצור/הפעל).
|
ניהול תהליכי-רקע (pm2) — כמו "שירותים" ב-Windows. דמון = שירות
|
||||||
תזמון (cron) = רץ לפי לוח-זמנים ("הרץ עכשיו" להרצה מיידית, ומתג
|
רץ-תמיד (הפעל-מחדש/עצור/הפעל). תזמון (cron) = רץ לפי לוח-זמנים
|
||||||
הפעלה/כיבוי של התזמון).
|
("הרץ עכשיו" להרצה מיידית, ומתג הפעלה/כיבוי של התזמון).
|
||||||
</p>
|
</p>
|
||||||
{data.services_error ? (
|
{data.services_error ? (
|
||||||
<p className="text-sm text-destructive">{data.services_error}</p>
|
<p className="text-sm text-destructive">{data.services_error}</p>
|
||||||
) : data.services.length === 0 ? (
|
) : data.services.length === 0 ? (
|
||||||
<p className="text-sm text-ink-muted">אין שירותים.</p>
|
<p className="text-sm text-ink-muted">אין שירותים.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-2">
|
/* mockup 02: services as a table — שירות · סטטוס · זמן-ריצה · זיכרון · controls */
|
||||||
{data.services.map((s: OpsService) => {
|
<div className="overflow-x-auto">
|
||||||
const isCron = !!s.cron;
|
<table className="w-full text-sm border-collapse">
|
||||||
return (
|
<thead>
|
||||||
<div
|
<tr className="border-b border-rule-soft text-ink-muted">
|
||||||
key={s.name}
|
<th className="text-start font-medium text-xs py-2 pe-3">שירות</th>
|
||||||
className="flex items-center justify-between gap-3 rounded-md border border-rule-soft bg-rule-soft/30 px-3 py-2"
|
<th className="text-start font-medium text-xs py-2 px-3">סטטוס</th>
|
||||||
>
|
<th className="text-start font-medium text-xs py-2 px-3 whitespace-nowrap">
|
||||||
<div className="min-w-0">
|
זמן-ריצה
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
</th>
|
||||||
{isCron ? (
|
<th className="text-start font-medium text-xs py-2 px-3 whitespace-nowrap">
|
||||||
<Badge
|
זיכרון / ↻
|
||||||
variant={s.disabled ? "destructive" : "default"}
|
</th>
|
||||||
className="font-normal"
|
<th className="py-2 ps-3" />
|
||||||
>
|
</tr>
|
||||||
{s.disabled ? "כבוי" : "פעיל (מתוזמן)"}
|
</thead>
|
||||||
</Badge>
|
<tbody>
|
||||||
) : (
|
{data.services.map((s: OpsService) => {
|
||||||
<StatusBadge value={s.status} />
|
const isCron = !!s.cron;
|
||||||
)}
|
return (
|
||||||
{s.cron ? (
|
<tr
|
||||||
<span className="text-[0.7rem] text-ink-muted font-mono" dir="ltr">
|
key={s.name}
|
||||||
{s.cron}
|
className="border-b border-rule-soft last:border-0 align-top"
|
||||||
</span>
|
>
|
||||||
) : null}
|
<td className="py-2.5 pe-3">
|
||||||
</div>
|
<div className="text-navy font-semibold text-[0.82rem]">
|
||||||
<div className="text-[0.8rem] text-navy truncate mt-0.5">
|
{SERVICE_LABELS[s.name] ?? s.name}
|
||||||
{SERVICE_LABELS[s.name] ?? s.name}
|
</div>
|
||||||
</div>
|
<div className="text-[0.66rem] text-ink-muted font-mono" dir="ltr">
|
||||||
<div className="text-[0.66rem] text-ink-muted flex items-center gap-2 flex-wrap">
|
{s.name}
|
||||||
<span className="font-mono" dir="ltr">
|
</div>
|
||||||
{s.name}
|
</td>
|
||||||
</span>
|
<td className="py-2.5 px-3">
|
||||||
<span>{mb(s.memory_bytes)}</span>
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span>↻{s.restarts}</span>
|
{isCron ? (
|
||||||
<span>{isCron ? `ריצה אחרונה ${ago(s.uptime_ms)}` : ago(s.uptime_ms)}</span>
|
<Badge
|
||||||
</div>
|
variant={s.disabled ? "destructive" : "default"}
|
||||||
</div>
|
className="font-normal"
|
||||||
<ServiceControls
|
>
|
||||||
s={s}
|
{s.disabled ? "כבוי" : "פעיל (מתוזמן)"}
|
||||||
busy={busy}
|
</Badge>
|
||||||
onAction={(a) => action.mutate({ name: s.name, action: a })}
|
) : (
|
||||||
onToggle={(disabled) => toggle.mutate({ name: s.name, disabled })}
|
<StatusBadge value={s.status} />
|
||||||
/>
|
)}
|
||||||
</div>
|
{s.cron ? (
|
||||||
);
|
<span
|
||||||
})}
|
className="text-[0.66rem] text-ink-muted font-mono"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{s.cron}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-[0.78rem] text-ink-soft whitespace-nowrap tabular-nums">
|
||||||
|
{isCron ? `אחרונה ${ago(s.uptime_ms)}` : ago(s.uptime_ms)}
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-[0.78rem] text-ink-soft whitespace-nowrap tabular-nums">
|
||||||
|
{mb(s.memory_bytes)} · ↻{s.restarts}
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 ps-3">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<ServiceControls
|
||||||
|
s={s}
|
||||||
|
busy={busy}
|
||||||
|
onAction={(a) => action.mutate({ name: s.name, action: a })}
|
||||||
|
onToggle={(disabled) => toggle.mutate({ name: s.name, disabled })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -336,6 +369,7 @@ function PipelineCard({
|
|||||||
children,
|
children,
|
||||||
href,
|
href,
|
||||||
hrefLabel,
|
hrefLabel,
|
||||||
|
gate = false,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
@@ -344,9 +378,18 @@ function PipelineCard({
|
|||||||
// (/approvals), never duplicated here. /operations only monitors.
|
// (/approvals), never duplicated here. /operations only monitors.
|
||||||
href?: string;
|
href?: string;
|
||||||
hrefLabel?: string;
|
hrefLabel?: string;
|
||||||
|
// mockup 02: gate cards get a gold-wash treatment + gold border so the
|
||||||
|
// human-gates read as distinct from the automatic pipelines.
|
||||||
|
gate?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card
|
||||||
|
className={
|
||||||
|
gate
|
||||||
|
? "bg-gold-wash border-gold/40 shadow-sm"
|
||||||
|
: "bg-surface border-rule shadow-sm"
|
||||||
|
}
|
||||||
|
>
|
||||||
<CardContent className="px-5 py-4 space-y-2.5">
|
<CardContent className="px-5 py-4 space-y-2.5">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@@ -356,7 +399,7 @@ function PipelineCard({
|
|||||||
{href && (
|
{href && (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="shrink-0 text-[0.72rem] text-gold-deep hover:underline whitespace-nowrap"
|
className="shrink-0 text-[0.72rem] text-gold-deep font-semibold hover:underline whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{hrefLabel ?? "לטיפול ←"}
|
{hrefLabel ?? "לטיפול ←"}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -456,30 +499,29 @@ function LiveAgentsPanel() {
|
|||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
<div className="flex items-center justify-between gap-3 mb-1 flex-wrap">
|
{/* mockup 02: status pills row (running / queued) + company hint */}
|
||||||
<h2 className="text-navy text-lg mb-0">סוכנים פעילים</h2>
|
{data ? (
|
||||||
{data ? (
|
<div className="flex items-center gap-2 text-[0.72rem] mb-3 flex-wrap">
|
||||||
<div className="flex items-center gap-2 text-[0.72rem]">
|
{/* ADM-6 (INV-IA5): counts sum only the companies that loaded.
|
||||||
{/* ADM-6 (INV-IA5): counts sum only the companies that loaded.
|
When a company errored, mark the totals as a floor ("+") so
|
||||||
When a company errored, mark the totals as a floor ("+") so
|
the operator isn't shown a shrunken depth as if complete. */}
|
||||||
the operator isn't shown a shrunken depth as if complete. */}
|
<Badge variant="default" className="font-normal">
|
||||||
<Badge variant="default" className="font-normal">
|
רצים {data.running}{data.errors.length > 0 ? "+" : ""}
|
||||||
רצים {data.running}{data.errors.length > 0 ? "+" : ""}
|
</Badge>
|
||||||
</Badge>
|
<Badge variant="secondary" className="font-normal">
|
||||||
<Badge variant="secondary" className="font-normal">
|
בתור {data.queued}{data.errors.length > 0 ? "+" : ""}
|
||||||
בתור {data.queued}{data.errors.length > 0 ? "+" : ""}
|
</Badge>
|
||||||
</Badge>
|
{data.errors.length > 0 ? (
|
||||||
{data.errors.length > 0 ? (
|
<span
|
||||||
<span
|
className="text-warn"
|
||||||
className="text-warn"
|
title={`ספירה חלקית — חברות שלא נטענו: ${data.errors.join(" · ")}`}
|
||||||
title={`ספירה חלקית — חברות שלא נטענו: ${data.errors.join(" · ")}`}
|
>
|
||||||
>
|
⚠ חלקי
|
||||||
⚠ חלקי
|
</span>
|
||||||
</span>
|
) : null}
|
||||||
) : null}
|
<span className="text-ink-muted ms-auto">חברות: CMP · CMPA</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
|
||||||
<p className="text-ink-muted text-xs mb-4">
|
<p className="text-ink-muted text-xs mb-4">
|
||||||
מי מבין סוכני-הוועדה עובד כרגע ומה הפלט שלו — כולל עבודה שלא קשורה לתיק (כמו
|
מי מבין סוכני-הוועדה עובד כרגע ומה הפלט שלו — כולל עבודה שלא קשורה לתיק (כמו
|
||||||
ריקון תור הלכות ע״י ה-CEO). עצירה היא מבוקרת דרך הפלטפורמה (לא kill).
|
ריקון תור הלכות ע״י ה-CEO). עצירה היא מבוקרת דרך הפלטפורמה (לא kill).
|
||||||
@@ -597,10 +639,14 @@ export default function OperationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<SectionHeader>סוכנים פעילים</SectionHeader>
|
||||||
<LiveAgentsPanel />
|
<LiveAgentsPanel />
|
||||||
|
|
||||||
|
<SectionHeader>שירותים</SectionHeader>
|
||||||
<ServicesPanel data={data} />
|
<ServicesPanel data={data} />
|
||||||
|
|
||||||
|
<SectionHeader>צינורות-עבודה</SectionHeader>
|
||||||
|
{/* Automatic pipelines — uniform stat cards */}
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
<PipelineCard
|
<PipelineCard
|
||||||
title="אחזור פסיקה (X13)"
|
title="אחזור פסיקה (X13)"
|
||||||
@@ -633,10 +679,14 @@ export default function OperationsPage() {
|
|||||||
לפסיקה
|
לפסיקה
|
||||||
</p>
|
</p>
|
||||||
</PipelineCard>
|
</PipelineCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* mockup 02: human-gate pointer cards — gold-wash, "לתיבת-האישורים ←" */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 mt-4">
|
||||||
<PipelineCard
|
<PipelineCard
|
||||||
|
gate
|
||||||
title="אישור הלכות (שער יו״ר)"
|
title="אישור הלכות (שער יו״ר)"
|
||||||
desc="הלכות שחולצו, ממתינות להכרעת דפנה — שער-אנושי, לא תהליך"
|
desc="שער-אנושי, לא תהליך — הפעולה ב-/approvals"
|
||||||
href="/approvals"
|
href="/approvals"
|
||||||
hrefLabel="לתיבת-האישורים ←"
|
hrefLabel="לתיבת-האישורים ←"
|
||||||
>
|
>
|
||||||
@@ -644,6 +694,7 @@ export default function OperationsPage() {
|
|||||||
</PipelineCard>
|
</PipelineCard>
|
||||||
|
|
||||||
<PipelineCard
|
<PipelineCard
|
||||||
|
gate
|
||||||
title="פסיקה חסרה"
|
title="פסיקה חסרה"
|
||||||
desc="פערים בקורפוס — נסגרים אוטומטית כשנקלטים"
|
desc="פערים בקורפוס — נסגרים אוטומטית כשנקלטים"
|
||||||
href="/approvals"
|
href="/approvals"
|
||||||
@@ -653,6 +704,7 @@ export default function OperationsPage() {
|
|||||||
</PipelineCard>
|
</PipelineCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SectionHeader>אחזורים אחרונים</SectionHeader>
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-5 py-4">
|
<CardContent className="px-5 py-4">
|
||||||
@@ -712,9 +764,9 @@ export default function OperationsPage() {
|
|||||||
|
|
||||||
{/* INV-IA4: the former /diagnostics surface, folded in here — one
|
{/* INV-IA4: the former /diagnostics surface, folded in here — one
|
||||||
monitoring intent, one surface. /diagnostics now redirects here. */}
|
monitoring intent, one surface. /diagnostics now redirects here. */}
|
||||||
<div className="space-y-3 pt-2">
|
<div className="pt-2">
|
||||||
<h2 className="text-navy text-lg mb-0">בריאות-מערכת</h2>
|
<SectionHeader>בריאות-מערכת</SectionHeader>
|
||||||
<p className="text-ink-muted text-sm">
|
<p className="text-ink-muted text-sm mb-3">
|
||||||
מצב ה-DB, תורי-אישור, ומסמכים שנכשלו/תקועים. (אוחד מ״אבחון״.)
|
מצב ה-DB, תורי-אישור, ומסמכים שנכשלו/תקועים. (אוחד מ״אבחון״.)
|
||||||
</p>
|
</p>
|
||||||
<SystemHealthSection />
|
<SystemHealthSection />
|
||||||
|
|||||||
@@ -9,8 +9,30 @@ import { AppealTypeBars, subtypeOf } from "@/components/cases/appeal-type-bars";
|
|||||||
import { CasesTable } from "@/components/cases/cases-table";
|
import { CasesTable } from "@/components/cases/cases-table";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useCases, type Case } from "@/lib/api/cases";
|
import { useCases, type Case, type CaseStatus } from "@/lib/api/cases";
|
||||||
import { usePendingApprovals } from "@/lib/api/chair";
|
import {
|
||||||
|
usePendingApprovals,
|
||||||
|
type ApprovalSeverity,
|
||||||
|
} from "@/lib/api/chair";
|
||||||
|
|
||||||
|
// severity dot per the approved 04-home mockup gate-list (.dot.high/.med/.ok)
|
||||||
|
const SEVERITY_DOT: Record<ApprovalSeverity, string> = {
|
||||||
|
high: "bg-danger",
|
||||||
|
medium: "bg-warn",
|
||||||
|
low: "bg-info",
|
||||||
|
ok: "bg-success",
|
||||||
|
};
|
||||||
|
|
||||||
|
// "תיקים לפי סטטוס" horizontal status bars (mockup 04: .bar / .track / .fill).
|
||||||
|
// Driven by the same live cases the KPI row uses — five status groups onto the
|
||||||
|
// gold/info/success/danger/muted palette.
|
||||||
|
type StatusBarRow = { label: string; fill: string; match: CaseStatus[] };
|
||||||
|
const STATUS_BARS: StatusBarRow[] = [
|
||||||
|
{ label: "בהכנה", fill: "bg-info", match: ["new", "uploading", "processing", "documents_ready", "analyst_verified", "research_complete", "outcome_set"] },
|
||||||
|
{ label: "ניתוח וכיוון", fill: "bg-gold", match: ["brainstorming", "direction_approved", "analysis_enriched", "ready_for_writing"] },
|
||||||
|
{ label: "בכתיבה", fill: "bg-warn", match: ["drafting", "qa_review", "drafted"] },
|
||||||
|
{ label: "הושלם", fill: "bg-success", match: ["exported", "reviewed", "final"] },
|
||||||
|
];
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { data, isPending, error } = useCases(true);
|
const { data, isPending, error } = useCases(true);
|
||||||
@@ -30,9 +52,19 @@ export default function HomePage() {
|
|||||||
return { permits, levies };
|
return { permits, levies };
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const statusBars = useMemo(() => {
|
||||||
|
const cases = data ?? [];
|
||||||
|
const counts = STATUS_BARS.map((b) => ({
|
||||||
|
...b,
|
||||||
|
n: cases.filter((c) => b.match.includes(c.status)).length,
|
||||||
|
}));
|
||||||
|
const max = Math.max(1, ...counts.map((c) => c.n));
|
||||||
|
return { counts, max };
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-8">
|
<section className="space-y-7">
|
||||||
<header className="flex items-end justify-between gap-6 flex-wrap">
|
<header className="flex items-end justify-between gap-6 flex-wrap">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
||||||
@@ -40,21 +72,53 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
<h1 className="text-navy">עוזר משפטי</h1>
|
<h1 className="text-navy">עוזר משפטי</h1>
|
||||||
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
|
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
|
||||||
לוח בקרה לניהול תיקי ערר, ניתוח סגנון, וכתיבת החלטות לפי ארכיטקטורת
|
מבט-על על הוועדה — תיקים, אישורים ופעילות אחרונה במקום אחד.
|
||||||
12 הבלוקים.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
<div className="flex gap-2.5">
|
||||||
<Link href="/cases/new">+ תיק חדש</Link>
|
<Button asChild className="bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||||
</Button>
|
<Link href="/cases/new">+ תיק חדש</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" className="border-rule text-navy">
|
||||||
|
<Link href="/precedents">חיפוש בקורפוס</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|
||||||
|
{/* KPI row — mockup 04 .kpis (4-up, gold-washed "ממתינים לאישור") */}
|
||||||
<KPICards cases={data} loading={isPending} />
|
<KPICards cases={data} loading={isPending} />
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
|
{/* two-column body — main flow + narrow gold gate rail (mockup 04 .cols) */}
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[1fr_360px]">
|
||||||
<div className="space-y-6 min-w-0">
|
<div className="space-y-6 min-w-0">
|
||||||
|
{/* תיקים לפי סטטוס — horizontal bars (mockup 04) */}
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5">
|
||||||
|
<h2 className="text-navy text-lg mb-4">תיקים לפי סטטוס</h2>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{statusBars.counts.map((b) => (
|
||||||
|
<li key={b.label} className="flex items-center gap-3">
|
||||||
|
<span className="w-20 shrink-0 text-[0.82rem] text-ink-soft">
|
||||||
|
{b.label}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 h-3.5 rounded-full bg-rule-soft overflow-hidden">
|
||||||
|
<span
|
||||||
|
className={`block h-full rounded-full ${b.fill} transition-[width] duration-500`}
|
||||||
|
style={{ width: `${(b.n / statusBars.max) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="w-9 shrink-0 text-end text-[0.82rem] font-semibold text-navy tabular-nums">
|
||||||
|
{isPending ? "—" : b.n}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* live case tables kept in full (richer than the mockup's single feed) */}
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
<div className="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
<div className="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||||
@@ -103,38 +167,39 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="space-y-6 lg:sticky lg:top-6 lg:self-start">
|
<aside className="space-y-6 lg:sticky lg:top-6 lg:self-start">
|
||||||
{approvals && approvals.total_pending > 0 ? (
|
{/* מה ממתין להכרעתך — gold gate card with dot+label+count rows (mockup 04 .gatecard) */}
|
||||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
<Card className="bg-gold-wash border-gold/50 shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
<div className="flex items-center justify-between gap-3 mb-3">
|
<h2 className="text-navy text-lg mb-3">מה ממתין להכרעתך</h2>
|
||||||
<h2 className="text-navy text-lg mb-0">מה ממתין להכרעתך</h2>
|
{approvals && approvals.categories.length > 0 ? (
|
||||||
<span className="text-2xl font-semibold text-gold-deep leading-none tabular-nums">
|
<ul className="mb-1">
|
||||||
{approvals.total_pending}
|
{approvals.categories.map((c) => (
|
||||||
</span>
|
<li
|
||||||
</div>
|
key={c.key}
|
||||||
<ul className="space-y-1.5 mb-4">
|
className="flex items-center gap-2.5 py-2.5 text-[0.88rem] text-ink-soft border-b border-rule-soft last:border-b-0"
|
||||||
{approvals.categories
|
>
|
||||||
.filter((c) => c.count > 0)
|
<span
|
||||||
.map((c) => (
|
className={`h-2.5 w-2.5 shrink-0 rounded-full ${SEVERITY_DOT[c.severity]}`}
|
||||||
<li
|
aria-hidden
|
||||||
key={c.key}
|
/>
|
||||||
className="flex items-center justify-between gap-2 text-[0.85rem] text-ink-soft"
|
<span className="grow min-w-0">{c.label}</span>
|
||||||
>
|
<span className="font-bold text-navy tabular-nums">{c.count}</span>
|
||||||
<span>{c.label}</span>
|
</li>
|
||||||
<span className="text-navy font-semibold tabular-nums">{c.count}</span>
|
))}
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
) : (
|
||||||
asChild
|
<p className="text-[0.85rem] text-ink-muted mb-2">
|
||||||
size="sm"
|
אין פריטים הממתינים להכרעתך.
|
||||||
className="bg-gold text-white hover:bg-gold-deep border-transparent w-full"
|
</p>
|
||||||
>
|
)}
|
||||||
<Link href="/approvals">למרכז האישורים ←</Link>
|
<Link
|
||||||
</Button>
|
href="/approvals"
|
||||||
</CardContent>
|
className="inline-block mt-3 text-[0.85rem] font-semibold text-gold-deep hover:text-navy"
|
||||||
</Card>
|
>
|
||||||
) : null}
|
למרכז האישורים ←
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import Link from "next/link";
|
|||||||
import { Pencil, Check, X, Share2 } from "lucide-react";
|
import { Pencil, Check, X, Share2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -34,6 +33,16 @@ const SOURCE_TYPE_LABELS: Record<string, string> = {
|
|||||||
appeals_committee: "ועדת ערר",
|
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.
|
/* Next 16 breaking change: route params are now a Promise.
|
||||||
* The `use()` hook unwraps them inside a client component. */
|
* The `use()` hook unwraps them inside a client component. */
|
||||||
export default function PrecedentDetailPage({
|
export default function PrecedentDetailPage({
|
||||||
@@ -48,187 +57,224 @@ export default function PrecedentDetailPage({
|
|||||||
const [editingCitation, setEditingCitation] = useState(false);
|
const [editingCitation, setEditingCitation] = useState(false);
|
||||||
const [citationDraft, setCitationDraft] = useState("");
|
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 (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6" dir="rtl">
|
<div dir="rtl">
|
||||||
<header>
|
{/* ── parchment header band (mockup 08 `.band`) — breaks out to the
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
AppShell <main> edges (px-10 py-10) for a full-width band. ──── */}
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-6">
|
||||||
<span aria-hidden> · </span>
|
<nav className="text-[0.78rem] text-ink-muted mb-2">
|
||||||
<Link href="/precedents" className="hover:text-gold-deep">ספריית פסיקה</Link>
|
<Link href="/precedents" className="text-gold-deep hover:underline">פסיקה</Link>
|
||||||
<span aria-hidden> · </span>
|
<span aria-hidden> ← </span>
|
||||||
<span className="text-navy">פרטי פסיקה</span>
|
<Link href="/precedents" className="text-gold-deep hover:underline">ספרייה</Link>
|
||||||
|
<span aria-hidden> ← </span>
|
||||||
|
<span>תקדים</span>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
|
||||||
|
|
||||||
{error ? (
|
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||||
<Card className="bg-danger-bg border-danger/40">
|
<div className="min-w-0">
|
||||||
<CardContent className="px-6 py-6 text-center space-y-3">
|
<h1 className="text-navy text-2xl font-bold leading-snug mb-1">
|
||||||
<p className="text-danger font-semibold">שגיאה בטעינת הפסיקה</p>
|
{data.case_name || "—"}
|
||||||
<p className="text-sm text-ink-muted">{error.message}</p>
|
</h1>
|
||||||
<Button asChild variant="outline">
|
<div className="text-ink-soft text-sm font-mono" dir="ltr">
|
||||||
<Link href="/precedents">חזרה לספרייה</Link>
|
{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>
|
||||||
</CardContent>
|
<Button variant="outline" size="sm" className="border-rule" onClick={() => setEditing(true)}>
|
||||||
</Card>
|
<Pencil className="w-3.5 h-3.5 me-1" /> ערוך פרטים
|
||||||
) : isPending || !data ? (
|
</Button>
|
||||||
<div className="space-y-3">
|
</div>
|
||||||
{[...Array(5)].map((_, i) => <Skeleton key={i} className="h-16 w-full" />)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-6 py-5 space-y-4">
|
|
||||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<h1 className="text-navy text-2xl font-semibold mb-1 leading-tight">
|
|
||||||
{data.case_name || "—"}
|
|
||||||
</h1>
|
|
||||||
<div className="text-ink-muted text-sm font-mono" dir="ltr">
|
|
||||||
{data.case_number}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button asChild variant="outline" size="sm">
|
|
||||||
<Link href={`/graph?focus=cl:${id}`}>
|
|
||||||
<Share2 className="w-3.5 h-3.5 me-1" /> הצג בגרף
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setEditing(true)}>
|
|
||||||
<Pencil className="w-3.5 h-3.5 me-1" /> ערוך פרטים
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Citation per Israeli unified citation rules. The LLM
|
{/* citation (unified Israeli citation rules) — chair-editable */}
|
||||||
extractor composes this from the document; the chair
|
<div className="mt-4 max-w-3xl">
|
||||||
can override below. */}
|
<CitationBlock
|
||||||
<CitationBlock
|
precedent={data as Precedent}
|
||||||
precedent={data as Precedent}
|
editing={editingCitation}
|
||||||
editing={editingCitation}
|
draft={citationDraft}
|
||||||
draft={citationDraft}
|
onStartEdit={() => {
|
||||||
onStartEdit={() => {
|
setCitationDraft(data.citation_formatted ?? "");
|
||||||
setCitationDraft(data.citation_formatted ?? "");
|
setEditingCitation(true);
|
||||||
setEditingCitation(true);
|
}}
|
||||||
}}
|
onCancel={() => setEditingCitation(false)}
|
||||||
onCancel={() => setEditingCitation(false)}
|
onChange={setCitationDraft}
|
||||||
onChange={setCitationDraft}
|
onSave={async () => {
|
||||||
onSave={async () => {
|
try {
|
||||||
try {
|
await update.mutateAsync({
|
||||||
await update.mutateAsync({
|
id,
|
||||||
id,
|
patch: { citation_formatted: citationDraft.trim() },
|
||||||
patch: { citation_formatted: citationDraft.trim() },
|
});
|
||||||
});
|
toast.success("מראה מקום עודכן");
|
||||||
toast.success("מראה מקום עודכן");
|
setEditingCitation(false);
|
||||||
setEditingCitation(false);
|
} catch (e) {
|
||||||
} catch (e) {
|
toast.error(e instanceof Error ? e.message : "שמירה נכשלה");
|
||||||
toast.error(
|
}
|
||||||
e instanceof Error ? e.message : "שמירה נכשלה",
|
}}
|
||||||
);
|
saving={update.isPending}
|
||||||
}
|
/>
|
||||||
}}
|
</div>
|
||||||
saving={update.isPending}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
{/* meta-band — label/value pairs + chips (mockup 08 `.metaband`) */}
|
||||||
{data.practice_area ? (
|
<div className="mt-4 flex flex-wrap items-center gap-x-6 gap-y-3">
|
||||||
<Badge variant="outline" className="text-[0.7rem] bg-info-bg text-info border-transparent">
|
{data.court ? <MetaItem label="בית-משפט">{data.court}</MetaItem> : null}
|
||||||
{PRACTICE_AREA_LABELS[data.practice_area] ?? data.practice_area}
|
{date ? (
|
||||||
</Badge>
|
<MetaItem label="תאריך">
|
||||||
) : null}
|
<span className="tabular-nums" dir="ltr">{date}</span>
|
||||||
{data.source_type ? (
|
</MetaItem>
|
||||||
<Badge variant="outline" className="text-[0.7rem]">
|
) : null}
|
||||||
{SOURCE_TYPE_LABELS[data.source_type] ?? data.source_type}
|
{data.practice_area ? (
|
||||||
</Badge>
|
<MetaItem label="תחום">
|
||||||
) : null}
|
<Badge variant="outline" className="text-[0.72rem] bg-info-bg text-info border-transparent rounded-full px-3">
|
||||||
{data.precedent_level ? (
|
{PRACTICE_AREA_LABELS[data.practice_area] ?? data.practice_area}
|
||||||
<Badge variant="outline" className="text-[0.7rem] bg-gold-wash text-gold-deep border-rule">
|
</Badge>
|
||||||
{data.precedent_level}
|
</MetaItem>
|
||||||
</Badge>
|
) : null}
|
||||||
) : null}
|
{data.source_type ? (
|
||||||
{data.is_binding ? (
|
<MetaItem label="סוג-מקור">
|
||||||
<Badge
|
<Badge variant="outline" className="text-[0.72rem] rounded-full px-3">
|
||||||
variant="outline"
|
{SOURCE_TYPE_LABELS[data.source_type] ?? data.source_type}
|
||||||
className="text-[0.7rem] bg-success-bg text-success border-transparent"
|
</Badge>
|
||||||
>
|
</MetaItem>
|
||||||
הלכה מחייבת
|
) : null}
|
||||||
</Badge>
|
{data.precedent_level ? (
|
||||||
) : null}
|
<MetaItem label="רמת-תקדים">
|
||||||
{data.court ? (
|
<Badge variant="outline" className="text-[0.72rem] bg-gold-wash text-gold-deep border-rule rounded-full px-3">
|
||||||
<span className="text-[0.78rem] text-ink-muted">{data.court}</span>
|
{data.precedent_level}
|
||||||
) : null}
|
</Badge>
|
||||||
{data.date ? (
|
</MetaItem>
|
||||||
<span className="text-[0.78rem] text-ink-muted tabular-nums" dir="ltr">
|
) : null}
|
||||||
{data.date.slice(0, 10)}
|
{data.is_binding ? (
|
||||||
</span>
|
<MetaItem label="סיווג-מחייבות">
|
||||||
) : null}
|
<Badge variant="outline" className="text-[0.72rem] bg-success-bg text-success border-transparent rounded-full px-3">
|
||||||
</div>
|
מחייב
|
||||||
|
</Badge>
|
||||||
|
</MetaItem>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
{data.headnote ? (
|
{data.subject_tags?.length ? (
|
||||||
<div>
|
<div className="mt-3 flex items-center gap-1 flex-wrap">
|
||||||
<h3 className="text-navy text-sm font-semibold m-0 mb-1">Headnote</h3>
|
{data.subject_tags.map((t) => (
|
||||||
<p className="text-ink-soft text-sm leading-relaxed m-0">
|
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
||||||
{data.headnote}
|
{t}
|
||||||
</p>
|
</Badge>
|
||||||
</div>
|
))}
|
||||||
) : null}
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
{data.summary ? (
|
{/* ── two-column body (mockup 08 `.wrap` grid) ────────────────── */}
|
||||||
<div>
|
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
|
||||||
<h3 className="text-navy text-sm font-semibold m-0 mb-1">תקציר</h3>
|
{/* main column */}
|
||||||
<p className="text-ink-soft text-sm leading-relaxed m-0 whitespace-pre-line">
|
<div className="space-y-4">
|
||||||
{data.summary}
|
{data.summary ? (
|
||||||
</p>
|
<DetailCard title="תקציר">
|
||||||
</div>
|
<p className="text-ink-soft text-sm leading-8 m-0 whitespace-pre-line">
|
||||||
) : null}
|
{data.summary}
|
||||||
|
</p>
|
||||||
|
</DetailCard>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{(data as { key_quote?: string }).key_quote ? (
|
{data.headnote ? (
|
||||||
<div>
|
<DetailCard title="כותרת-הלכה (headnote)" prov="opus">
|
||||||
<h3 className="text-navy text-sm font-semibold m-0 mb-1">ציטוט מרכזי</h3>
|
<p className="text-ink-soft text-sm leading-8 m-0">{data.headnote}</p>
|
||||||
<blockquote className="text-ink-soft text-sm leading-relaxed border-s-[3px] border-gold bg-gold-wash ps-3 pe-4 py-3 rounded-e m-0">
|
</DetailCard>
|
||||||
{(data as { key_quote?: string }).key_quote}
|
) : null}
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{data.subject_tags?.length ? (
|
{(data as { key_quote?: string }).key_quote ? (
|
||||||
<div className="flex items-center gap-1 flex-wrap pt-1">
|
<DetailCard title="ציטוט-מפתח" prov="opus">
|
||||||
{data.subject_tags.map((t) => (
|
<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">
|
||||||
<Badge key={t} variant="outline" className="text-[0.65rem]">
|
{(data as { key_quote?: string }).key_quote}
|
||||||
{t}
|
</blockquote>
|
||||||
</Badge>
|
</DetailCard>
|
||||||
))}
|
) : null}
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<div className="rounded-lg border border-rule bg-surface shadow-sm px-5 py-4">
|
||||||
<CardContent className="px-6 py-5">
|
<ExtractedHalachotSection halachot={data.halachot ?? []} />
|
||||||
<RelatedCasesSection
|
</div>
|
||||||
caseId={id}
|
</div>
|
||||||
related={data.related_cases ?? []}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
{/* side rail — citations + corroboration */}
|
||||||
<CardContent className="px-6 py-5">
|
<div className="space-y-4">
|
||||||
<ExtractedHalachotSection halachot={data.halachot ?? []} />
|
<RelatedCasesSection caseId={id} related={data.related_cases ?? []} />
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<PrecedentEditSheet
|
<PrecedentEditSheet
|
||||||
caseLawId={editing ? id : null}
|
caseLawId={editing ? id : null}
|
||||||
onOpenChange={(open) => setEditing(open)}
|
onOpenChange={(open) => setEditing(open)}
|
||||||
/>
|
/>
|
||||||
</section>
|
|
||||||
</AppShell>
|
</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({
|
function CitationBlock({
|
||||||
precedent,
|
precedent,
|
||||||
editing,
|
editing,
|
||||||
@@ -252,7 +298,7 @@ function CitationBlock({
|
|||||||
|
|
||||||
if (editing) {
|
if (editing) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-gold/40 bg-gold-wash/30 p-3 space-y-2">
|
<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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[0.78rem] font-semibold text-navy">
|
<span className="text-[0.78rem] font-semibold text-navy">
|
||||||
עריכת מראה מקום
|
עריכת מראה מקום
|
||||||
@@ -266,7 +312,7 @@ function CitationBlock({
|
|||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm bg-surface"
|
||||||
placeholder='ערר (ועדות ערר ...) 1234/24 **עורר נ' הוועדה המקומית** (נבו 1.2.2025)'
|
placeholder='ערר (ועדות ערר ...) 1234/24 **עורר נ' הוועדה המקומית** (נבו 1.2.2025)'
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
@@ -280,12 +326,7 @@ function CitationBlock({
|
|||||||
<Check className="w-3.5 h-3.5 me-1" />
|
<Check className="w-3.5 h-3.5 me-1" />
|
||||||
שמור
|
שמור
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" variant="outline" onClick={onCancel} disabled={saving}>
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
<X className="w-3.5 h-3.5 me-1" />
|
<X className="w-3.5 h-3.5 me-1" />
|
||||||
ביטול
|
ביטול
|
||||||
</Button>
|
</Button>
|
||||||
@@ -296,7 +337,7 @@ function CitationBlock({
|
|||||||
|
|
||||||
if (!citation) {
|
if (!citation) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-dashed border-rule bg-rule-soft/30 p-3 flex items-center justify-between gap-2">
|
<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 className="text-[0.78rem] text-ink-muted">
|
||||||
מראה מקום (כללי הציטוט האחיד) — טרם חולץ
|
מראה מקום (כללי הציטוט האחיד) — טרם חולץ
|
||||||
</span>
|
</span>
|
||||||
@@ -309,7 +350,7 @@ function CitationBlock({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-rule bg-parchment-50 p-3 space-y-1.5">
|
<div className="rounded-md border border-rule bg-surface p-3 space-y-1.5">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[0.7rem] uppercase tracking-wide text-ink-muted">
|
<span className="text-[0.7rem] uppercase tracking-wide text-ink-muted">
|
||||||
מראה מקום
|
מראה מקום
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AppShell } from "@/components/app-shell";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { LibraryListPanel } from "@/components/precedents/library-list-panel";
|
import { LibraryListPanel } from "@/components/precedents/library-list-panel";
|
||||||
import { LibrarySearchPanel } from "@/components/precedents/library-search-panel";
|
import { LibrarySearchPanel } from "@/components/precedents/library-search-panel";
|
||||||
import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel";
|
import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel";
|
||||||
import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel";
|
import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel";
|
||||||
import { useHalachotPending } from "@/lib/api/precedent-library";
|
import { useHalachotPending } from "@/lib/api/precedent-library";
|
||||||
|
import { useMissingPrecedents } from "@/lib/api/missing-precedents";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Precedent Library admin page.
|
* Precedent Library admin page.
|
||||||
@@ -25,72 +24,133 @@ import { useHalachotPending } from "@/lib/api/precedent-library";
|
|||||||
* per-case precedent attacher (chair-attached quotes scoped to a case).
|
* per-case precedent attacher (chair-attached quotes scoped to a case).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function PendingBadge() {
|
/** Colored count pill riding on a tab trigger (mockup 07: warn for review
|
||||||
const { data } = useHalachotPending();
|
* queue, info for incoming). Returns null when the queue is empty. */
|
||||||
const n = data?.count ?? 0;
|
function CountPill({ n, tone }: { n: number; tone: "warn" | "info" }) {
|
||||||
if (!n) return null;
|
if (!n) return null;
|
||||||
|
const cls =
|
||||||
|
tone === "warn"
|
||||||
|
? "bg-warn text-white"
|
||||||
|
: "bg-info text-white";
|
||||||
return (
|
return (
|
||||||
<Badge
|
<span
|
||||||
variant="outline"
|
className={`ms-1.5 inline-flex items-center justify-center rounded-full px-1.5 min-w-[1.15rem] h-[1.15rem] text-[0.68rem] font-semibold tabular-nums ${cls}`}
|
||||||
className="ms-1 bg-gold-wash text-gold-deep border-gold/40 text-[0.65rem]"
|
|
||||||
>
|
>
|
||||||
{n}
|
{n}
|
||||||
</Badge>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PendingPill() {
|
||||||
|
const { data } = useHalachotPending();
|
||||||
|
return <CountPill n={data?.count ?? 0} tone="warn" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IncomingPill() {
|
||||||
|
// "פסיקה נכנסת" = open missing-precedents waiting for the chair to upload.
|
||||||
|
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
|
||||||
|
return <CountPill n={data?.by_status?.open ?? 0} tone="info" />;
|
||||||
|
}
|
||||||
|
|
||||||
export default function PrecedentsPage() {
|
export default function PrecedentsPage() {
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
<Tabs defaultValue="library" dir="rtl">
|
||||||
<header>
|
<section className="space-y-6">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<header className="space-y-3">
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<nav className="text-[0.78rem] text-ink-muted">
|
||||||
<span aria-hidden> · </span>
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<span className="text-navy">ספריית פסיקה</span>
|
<span aria-hidden> · </span>
|
||||||
</nav>
|
<span className="text-navy">ספריית פסיקה</span>
|
||||||
<h1 className="text-navy mb-0">ספריית הפסיקה הסמכותית</h1>
|
</nav>
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl">
|
<div className="space-y-1">
|
||||||
פסיקה חיצונית — פסקי דין של ערכאות עליונות והחלטות של ועדות ערר אחרות.
|
<h1 className="text-navy mb-0">ספריית הפסיקה הסמכותית</h1>
|
||||||
כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות לאישור היו"ר לפני
|
<p className="text-ink-muted text-sm mt-1 max-w-3xl leading-relaxed">
|
||||||
שהן זמינות לסוכני הכתיבה (legal-writer וכו').
|
קורפוס הפסיקה והלכות המערכת — חיפוש סמנטי, תור-אישור והשלמת
|
||||||
</p>
|
פסיקה חסרה. כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות
|
||||||
</header>
|
לאישור היו"ר לפני שהן זמינות לסוכני הכתיבה.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
{/* tabs as a dedicated row under the header — underline-style
|
||||||
|
triggers with colored count pills (mockup 07). */}
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<TabsList className="flex w-full justify-start gap-1 rounded-none border-0 border-b border-rule bg-transparent p-0 h-auto">
|
||||||
<CardContent className="px-6 py-5">
|
{[
|
||||||
<Tabs defaultValue="library" dir="rtl">
|
{ value: "library", label: "ספרייה", pill: null },
|
||||||
<TabsList className="bg-rule-soft/60">
|
{ value: "search", label: "חיפוש בקורפוס", pill: null },
|
||||||
<TabsTrigger value="library">ספרייה</TabsTrigger>
|
{ value: "review", label: "תור הלכות", pill: <PendingPill /> },
|
||||||
<TabsTrigger value="search">חיפוש סמנטי</TabsTrigger>
|
{
|
||||||
<TabsTrigger value="review">
|
value: "incoming",
|
||||||
ממתין לאישור
|
label: "פסיקה נכנסת",
|
||||||
<PendingBadge />
|
pill: <IncomingPill />,
|
||||||
|
},
|
||||||
|
{ value: "stats", label: "סטטיסטיקה", pill: null },
|
||||||
|
].map((t) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={t.value}
|
||||||
|
value={t.value}
|
||||||
|
className="rounded-none border-0 border-b-2 border-transparent bg-transparent px-4 py-2.5 -mb-px text-sm font-medium text-ink-muted shadow-none data-[state=active]:border-gold data-[state=active]:bg-transparent data-[state=active]:font-semibold data-[state=active]:text-navy data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
{t.pill}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="stats">סטטיסטיקה</TabsTrigger>
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
</header>
|
||||||
|
|
||||||
<TabsContent value="library" className="mt-5">
|
<TabsContent value="library" className="mt-0">
|
||||||
<LibraryListPanel />
|
<LibraryListPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="search" className="mt-5">
|
<TabsContent value="search" className="mt-0">
|
||||||
<LibrarySearchPanel />
|
<LibrarySearchPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="review" className="mt-5">
|
<TabsContent value="review" className="mt-0">
|
||||||
<HalachaReviewPanel />
|
<HalachaReviewPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="stats" className="mt-5">
|
{/* "פסיקה נכנסת" — the incoming/missing-precedent queue. Kept as a
|
||||||
<LibraryStatsPanel />
|
tab per the mockup; full management lives on /missing-precedents. */}
|
||||||
</TabsContent>
|
<TabsContent value="incoming" className="mt-0">
|
||||||
</Tabs>
|
<IncomingTab />
|
||||||
</CardContent>
|
</TabsContent>
|
||||||
</Card>
|
|
||||||
</section>
|
<TabsContent value="stats" className="mt-0">
|
||||||
|
<LibraryStatsPanel />
|
||||||
|
</TabsContent>
|
||||||
|
</section>
|
||||||
|
</Tabs>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lightweight in-tab pointer to the dedicated missing-precedents page,
|
||||||
|
* preserving the mockup's "פסיקה נכנסת" tab without duplicating the table. */
|
||||||
|
function IncomingTab() {
|
||||||
|
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
|
||||||
|
const open = data?.by_status?.open ?? 0;
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-rule bg-surface shadow-sm p-6 space-y-3">
|
||||||
|
<div className="flex items-baseline gap-3 flex-wrap">
|
||||||
|
<h2 className="text-navy text-lg font-semibold m-0">פסיקה נכנסת</h2>
|
||||||
|
{open ? (
|
||||||
|
<span className="inline-flex items-baseline gap-1.5 rounded-lg border border-rule bg-warn-bg px-3 py-1">
|
||||||
|
<span className="text-base font-bold text-warn tabular-nums">{open}</span>
|
||||||
|
<span className="text-[0.8rem] text-ink-soft">פתוחים</span>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p className="text-ink-soft text-sm leading-relaxed max-w-2xl">
|
||||||
|
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
|
||||||
|
אימות-הלכה ועיגון-מקור (INV-AH).
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/missing-precedents"
|
||||||
|
className="inline-flex items-center rounded-md bg-gold px-4 py-2 text-sm font-semibold text-white hover:bg-gold-deep"
|
||||||
|
>
|
||||||
|
לניהול פסיקה חסרה ←
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,133 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
import { fetchScriptsCatalog } from "@/lib/api/scripts";
|
import { fetchScriptsCatalog } from "@/lib/api/scripts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* /scripts — read-only catalog of everything under scripts/.
|
* /scripts — catalog of everything under scripts/, rendered as the
|
||||||
|
* approved IA-redesign table (name mono · role · status chip · run/source
|
||||||
|
* ghost button).
|
||||||
*
|
*
|
||||||
* The content is `scripts/SCRIPTS.md` verbatim (active · archived · deleted
|
* The single source of truth is still `scripts/SCRIPTS.md` (CLAUDE.md mandates
|
||||||
* tables), served by GET /api/scripts/catalog. SCRIPTS.md is the single
|
* updating it on every script change), served verbatim by
|
||||||
* source of truth — CLAUDE.md mandates updating it on every script change —
|
* GET /api/scripts/catalog. We parse its markdown tables into structured rows
|
||||||
* so we render it directly rather than re-describing the scripts here.
|
* for display — editing remains git/Gitea only, so the per-row "מקור" button
|
||||||
|
* deep-links to the file in Gitea rather than inventing a run-from-UI mutation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type ScriptStatus = "active" | "once" | "archive" | "deleted";
|
||||||
|
|
||||||
|
type ScriptRow = {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
status: ScriptStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_LABEL: Record<ScriptStatus, string> = {
|
||||||
|
active: "פעיל",
|
||||||
|
once: "חד-פעמי",
|
||||||
|
archive: "ארכיון",
|
||||||
|
deleted: "נמחק",
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_TONE: Record<ScriptStatus, { wrap: string; dot: string }> = {
|
||||||
|
active: { wrap: "bg-success-bg text-success", dot: "bg-success" },
|
||||||
|
once: { wrap: "bg-info-bg text-info", dot: "bg-info" },
|
||||||
|
archive: { wrap: "bg-rule-soft text-ink-muted", dot: "bg-ink-muted" },
|
||||||
|
deleted: { wrap: "bg-danger-bg text-danger", dot: "bg-danger" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// "חד-פעמי" / "one-shot" markers inside the Scheduled column of an active row.
|
||||||
|
const ONCE_RE = /חד-?פעמי|one-?shot|בוצע/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse SCRIPTS.md markdown tables into typed rows. The file has three
|
||||||
|
* sections with different shapes; we read the first two columns of each
|
||||||
|
* (name + role) and derive status from the section + scheduling note.
|
||||||
|
*/
|
||||||
|
function parseScripts(md: string): ScriptRow[] {
|
||||||
|
const lines = md.split("\n");
|
||||||
|
const rows: ScriptRow[] = [];
|
||||||
|
let section: ScriptStatus = "active";
|
||||||
|
|
||||||
|
for (const raw of lines) {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (line.startsWith("## ")) {
|
||||||
|
if (line.includes(".archive") || line.includes("הושלמו")) section = "archive";
|
||||||
|
else if (line.includes("נמחק")) section = "deleted";
|
||||||
|
else section = "active";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!line.startsWith("|")) continue;
|
||||||
|
// skip header + separator rows
|
||||||
|
const cells = line
|
||||||
|
.split("|")
|
||||||
|
.slice(1, -1)
|
||||||
|
.map((c) => c.trim());
|
||||||
|
if (cells.length < 2) continue;
|
||||||
|
if (/^-+$/.test(cells[0].replace(/[-:]/g, "-"))) continue; // separator
|
||||||
|
if (cells[0] === "Script") continue; // header
|
||||||
|
if (!cells[0]) continue;
|
||||||
|
|
||||||
|
const name = cells[0].replace(/`/g, "");
|
||||||
|
if (!name) continue;
|
||||||
|
|
||||||
|
let status: ScriptStatus = section;
|
||||||
|
if (section === "active") {
|
||||||
|
const scheduled = cells[3] ?? "";
|
||||||
|
status = ONCE_RE.test(scheduled) ? "once" : "active";
|
||||||
|
}
|
||||||
|
// role: active = Purpose (col 2), archive = Original Purpose (col 1),
|
||||||
|
// deleted = Reason (col 1).
|
||||||
|
const role =
|
||||||
|
section === "active" ? cells[2] ?? cells[1] ?? "" : cells[1] ?? "";
|
||||||
|
|
||||||
|
rows.push({ name, role: stripMd(role), status });
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip bold/inline-code markdown so the role reads as plain text in a cell.
|
||||||
|
function stripMd(s: string): string {
|
||||||
|
return s.replace(/\*\*/g, "").replace(/`/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusChip({ status }: { status: ScriptStatus }) {
|
||||||
|
const tone = STATUS_TONE[status];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[0.75rem] font-semibold ${tone.wrap}`}
|
||||||
|
>
|
||||||
|
<span className={`h-1.5 w-1.5 rounded-full ${tone.dot}`} aria-hidden />
|
||||||
|
{STATUS_LABEL[status]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function ScriptsPage() {
|
export default function ScriptsPage() {
|
||||||
const { data, isLoading, isError, error } = useQuery({
|
const { data, isLoading, isError, error } = useQuery({
|
||||||
queryKey: ["scripts-catalog"],
|
queryKey: ["scripts-catalog"],
|
||||||
queryFn: ({ signal }) => fetchScriptsCatalog(signal),
|
queryFn: ({ signal }) => fetchScriptsCatalog(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rows = useMemo(
|
||||||
|
() => (data?.content ? parseScripts(data.content) : []),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
const lastModified =
|
const lastModified =
|
||||||
data?.last_modified != null
|
data?.last_modified != null
|
||||||
? new Date(data.last_modified * 1000).toLocaleDateString("he-IL", {
|
? new Date(data.last_modified * 1000).toLocaleDateString("he-IL", {
|
||||||
@@ -31,63 +137,113 @@ export default function ScriptsPage() {
|
|||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const giteaBase = data?.gitea_url ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<div className="flex items-end justify-between gap-4">
|
<header>
|
||||||
<div>
|
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<span aria-hidden> · </span>
|
||||||
<span aria-hidden> · </span>
|
<span className="text-navy">סקריפטים</span>
|
||||||
<span className="text-navy">סקריפטים</span>
|
</nav>
|
||||||
</nav>
|
<h1 className="text-navy mb-0">סקריפטים</h1>
|
||||||
<h1 className="text-navy mb-0">סקריפטים</h1>
|
<p className="text-sm text-ink-muted mt-1 max-w-2xl">
|
||||||
<p className="text-sm text-ink-muted mt-1">
|
סקריפטי-תחזוקה ותפעול. מקור-האמת הוא{" "}
|
||||||
קטלוג כל הסקריפטים בתיקיית{" "}
|
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
|
||||||
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
|
scripts/SCRIPTS.md
|
||||||
scripts/
|
</code>{" "}
|
||||||
</code>{" "}
|
— עריכה דרך git, לא מכאן.
|
||||||
— שם, סוג, תפקיד ותזמון. מקור-האמת הוא{" "}
|
</p>
|
||||||
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
|
</header>
|
||||||
scripts/SCRIPTS.md
|
|
||||||
</code>
|
|
||||||
; עריכה דרך git, לא מכאן.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{data?.gitea_url ? (
|
|
||||||
<a
|
|
||||||
href={data.gitea_url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="shrink-0 text-sm text-gold-deep hover:text-gold underline underline-offset-2"
|
|
||||||
>
|
|
||||||
מקור ב-Gitea ↗
|
|
||||||
</a>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
{isLoading ? (
|
||||||
<CardContent className="px-6 py-5">
|
<Card className="bg-surface border-rule px-6 py-5 text-sm text-ink-muted">
|
||||||
{isLoading ? (
|
טוען קטלוג…
|
||||||
<p className="text-sm text-ink-muted">טוען קטלוג…</p>
|
</Card>
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<p className="text-sm text-danger">
|
<Card className="bg-danger-bg border-danger/40 px-6 py-5 text-sm text-danger">
|
||||||
שגיאה בטעינת הקטלוג: {(error as Error)?.message ?? "לא ידוע"}
|
שגיאה בטעינת הקטלוג: {(error as Error)?.message ?? "לא ידוע"}
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-parchment hover:bg-parchment border-rule">
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
שם הסקריפט
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
תפקיד
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
סטטוס
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-end text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
פעולה
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map((s) => {
|
||||||
|
const disabled = s.status === "archive" || s.status === "deleted";
|
||||||
|
const href = giteaBase
|
||||||
|
? `${giteaBase.replace(/\/$/, "")}/${s.name}`
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={s.name}
|
||||||
|
className="border-rule-soft hover:bg-gold-wash align-middle"
|
||||||
|
>
|
||||||
|
<TableCell className="px-5 py-3.5">
|
||||||
|
<code
|
||||||
|
className="font-mono text-[0.81rem] font-semibold text-navy"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{s.name}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-5 py-3.5 text-ink-soft text-[0.84rem] leading-snug max-w-xl whitespace-normal">
|
||||||
|
<span className="line-clamp-2">{s.role}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-5 py-3.5">
|
||||||
|
<StatusChip status={s.status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-5 py-3.5 text-end">
|
||||||
|
{disabled || !href ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
className="rounded-lg border border-rule-soft px-4 py-1.5 text-[0.81rem] font-semibold text-ink-muted cursor-default"
|
||||||
|
>
|
||||||
|
מקור
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-block rounded-lg border border-rule px-4 py-1.5 text-[0.81rem] font-semibold text-gold-deep hover:bg-gold-wash hover:border-gold transition-colors"
|
||||||
|
>
|
||||||
|
מקור
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{lastModified ? (
|
||||||
|
<p className="px-5 py-3 border-t border-rule text-xs text-ink-muted">
|
||||||
|
עודכן לאחרונה: {lastModified}
|
||||||
</p>
|
</p>
|
||||||
) : data ? (
|
|
||||||
<>
|
|
||||||
<Markdown content={data.content} />
|
|
||||||
{lastModified ? (
|
|
||||||
<p className="mt-6 pt-3 border-t border-rule text-xs text-ink-muted">
|
|
||||||
עודכן לאחרונה: {lastModified}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
)}
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AlertTriangle, CheckCircle2, HelpCircle } from "lucide-react";
|
import { AlertTriangle, CheckCircle2, HelpCircle } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
drift: boolean;
|
drift: boolean;
|
||||||
@@ -9,31 +8,31 @@ type Props = {
|
|||||||
coolifyAvailable?: boolean;
|
coolifyAvailable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filled pill chips matching IA-redesign mockup 15 (.c-synced / .c-drift).
|
||||||
export function DriftBadge({ drift, coolifyAvailable = true }: Props) {
|
export function DriftBadge({ drift, coolifyAvailable = true }: Props) {
|
||||||
if (!coolifyAvailable) {
|
if (!coolifyAvailable) {
|
||||||
return (
|
return (
|
||||||
<Badge
|
<span
|
||||||
variant="outline"
|
className="inline-flex items-center gap-1 rounded-full bg-rule-soft text-ink-muted text-[0.72rem] font-semibold px-2.5 py-0.5"
|
||||||
className="text-ink-muted border-rule gap-1"
|
|
||||||
title="Coolify לא זמין — מצב ה-drift לא ידוע"
|
title="Coolify לא זמין — מצב ה-drift לא ידוע"
|
||||||
>
|
>
|
||||||
<HelpCircle className="w-3 h-3" />
|
<HelpCircle className="w-3 h-3" />
|
||||||
Unknown
|
Unknown
|
||||||
</Badge>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (drift) {
|
if (drift) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className="text-warn border-warn/40 gap-1">
|
<span className="inline-flex items-center gap-1 rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
<AlertTriangle className="w-3 h-3" />
|
<AlertTriangle className="w-3 h-3" />
|
||||||
Drift
|
Drift
|
||||||
</Badge>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className="text-success border-success/40 gap-1">
|
<span className="inline-flex items-center gap-1 rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
<CheckCircle2 className="w-3 h-3" />
|
<CheckCircle2 className="w-3 h-3" />
|
||||||
Synced
|
Synced
|
||||||
</Badge>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ExternalLink, Save, Lock } from "lucide-react";
|
import { ExternalLink, Save, Lock } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import type { McpEnvVar } from "@/lib/api/settings";
|
import type { McpEnvVar } from "@/lib/api/settings";
|
||||||
import { useUpdateMcpEnv } from "@/lib/api/settings";
|
import { useUpdateMcpEnv } from "@/lib/api/settings";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -44,36 +43,38 @@ export function EnvVarRow({
|
|||||||
`https://coolify.nautilus.marcusgroup.org/project/applications/${coolifyAppUuid}/environment-variables`;
|
`https://coolify.nautilus.marcusgroup.org/project/applications/${coolifyAppUuid}/environment-variables`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-rule p-4 bg-rule-soft/20 hover:bg-rule-soft/40 transition-colors">
|
<div className="px-5 py-4 border-b border-rule-soft last:border-b-0">
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
{/* envtop — key + type chip + secret chip + drift/synced chip */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<code className="font-mono text-[0.84rem] font-semibold text-navy" dir="ltr">
|
||||||
<code className="font-mono text-sm font-medium text-navy" dir="ltr">
|
{spec.key}
|
||||||
{spec.key}
|
</code>
|
||||||
</code>
|
<span className="inline-flex items-center rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
<Badge variant="outline" className="text-[0.7rem]">
|
{spec.type}
|
||||||
{spec.type}
|
</span>
|
||||||
</Badge>
|
{spec.is_secret && (
|
||||||
{spec.is_secret && (
|
<span className="inline-flex items-center gap-1 rounded-full bg-navy text-white text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
<Badge variant="outline" className="text-[0.7rem] text-warn border-warn/40 gap-1">
|
<Lock className="w-3 h-3" />
|
||||||
<Lock className="w-3 h-3" />
|
secret
|
||||||
secret
|
</span>
|
||||||
</Badge>
|
)}
|
||||||
)}
|
<DriftBadge drift={spec.drift} coolifyAvailable={coolifyAvailable} />
|
||||||
<DriftBadge drift={spec.drift} coolifyAvailable={coolifyAvailable} />
|
{spec.has_duplicates && (
|
||||||
{spec.has_duplicates && (
|
<span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
<Badge variant="outline" className="text-[0.7rem] text-warn border-warn/40">
|
duplicates
|
||||||
duplicates
|
</span>
|
||||||
</Badge>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-ink-muted mt-1">{spec.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{spec.description && (
|
||||||
|
<p className="text-[0.82rem] text-ink-muted mt-1">{spec.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
{/* vals — Coolify value | Container value (+pending) | save (mockup .vals 3-col) */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="grid grid-cols-1 md:grid-cols-[1fr_1fr_auto] gap-3.5 items-end mt-3.5">
|
||||||
<span className="text-[0.72rem] text-ink-muted w-20">Coolify:</span>
|
<div>
|
||||||
|
<span className="block text-[0.72rem] text-ink-muted font-semibold mb-1">
|
||||||
|
Coolify:
|
||||||
|
</span>
|
||||||
{spec.is_editable ? (
|
{spec.is_editable ? (
|
||||||
<EnvVarEditor
|
<EnvVarEditor
|
||||||
spec={spec}
|
spec={spec}
|
||||||
@@ -82,53 +83,61 @@ export function EnvVarRow({
|
|||||||
disabled={update.isPending}
|
disabled={update.isPending}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="font-mono text-ink" dir="ltr">
|
<a
|
||||||
{spec.coolify_value ?? <em className="text-ink-muted">— לא מוגדר —</em>}
|
href={coolifyEnvUrl}
|
||||||
</span>
|
target="_blank"
|
||||||
)}
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="font-mono text-[0.81rem] text-gold-deep hover:underline flex items-center gap-1"
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
dir="ltr"
|
||||||
<span className="text-[0.72rem] text-ink-muted w-20">Container:</span>
|
|
||||||
<span className="font-mono text-ink" dir="ltr">
|
|
||||||
{spec.container_value ?? <em className="text-ink-muted">— לא מוגדר —</em>}
|
|
||||||
</span>
|
|
||||||
{/* ADM-5 (INV-IA5/INV-IA6): when Coolify ≠ Container the container is
|
|
||||||
running a stale value until a redeploy — say so in plain Hebrew
|
|
||||||
right here, not only via the top "Drift" badge. */}
|
|
||||||
{coolifyAvailable && spec.drift && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-[0.7rem] text-warn border-warn/40"
|
|
||||||
title="הערך נשמר ב-Coolify אך הקונטיינר עדיין מריץ את הקודם — נדרש redeploy"
|
|
||||||
>
|
>
|
||||||
ממתין ל-redeploy
|
{spec.coolify_value ?? "— לא מוגדר —"}
|
||||||
</Badge>
|
<ExternalLink className="w-3 h-3 shrink-0" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[0.72rem] text-ink-muted font-semibold mb-1">
|
||||||
|
Container:
|
||||||
|
</span>
|
||||||
|
<div className="font-mono text-[0.81rem] text-ink-soft bg-rule-soft rounded-md px-3 py-2 flex items-center gap-2 flex-wrap" dir="ltr">
|
||||||
|
<span>{spec.container_value ?? "— לא מוגדר —"}</span>
|
||||||
|
{/* ADM-5 (INV-IA5/INV-IA6): Coolify ≠ Container ⇒ container is stale
|
||||||
|
until a redeploy — say so in plain Hebrew right here. */}
|
||||||
|
{coolifyAvailable && spec.drift && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center rounded-full bg-warn-bg text-warn border border-warn text-[0.7rem] font-semibold px-2 py-0.5"
|
||||||
|
dir="rtl"
|
||||||
|
title="הערך נשמר ב-Coolify אך הקונטיינר עדיין מריץ את הקודם — נדרש redeploy"
|
||||||
|
>
|
||||||
|
ממתין ל-redeploy
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex md:block items-end">
|
||||||
|
{spec.is_editable ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!dirty || update.isPending}
|
||||||
|
variant={dirty ? "default" : "outline"}
|
||||||
|
className={dirty ? "bg-gold text-white hover:bg-gold-deep border-transparent" : ""}
|
||||||
|
>
|
||||||
|
<Save className="w-3.5 h-3.5" data-icon="inline-start" />
|
||||||
|
{update.isPending ? "שומר..." : "שמור"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={coolifyEnvUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 rounded-md border border-rule text-navy px-4 py-1.5 text-[0.81rem] font-semibold hover:bg-gold-wash"
|
||||||
|
>
|
||||||
|
ערוך ב-Coolify
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 mt-3">
|
|
||||||
{!spec.is_editable && (
|
|
||||||
<a
|
|
||||||
href={coolifyEnvUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-[0.78rem] text-gold-deep hover:underline flex items-center gap-1"
|
|
||||||
>
|
|
||||||
ערוך ב-Coolify
|
|
||||||
<ExternalLink className="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{spec.is_editable && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={!dirty || update.isPending}
|
|
||||||
>
|
|
||||||
<Save className="w-3.5 h-3.5" data-icon="inline-start" />
|
|
||||||
{update.isPending ? "שומר..." : "שמור"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,37 +72,39 @@ export function EnvironmentTab() {
|
|||||||
const duplicatesCount = data.vars.filter((v) => v.has_duplicates).length;
|
const duplicatesCount = data.vars.filter((v) => v.has_duplicates).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-5">
|
||||||
<Card className="bg-surface border-rule">
|
{/* summary band — Coolify app id + drift/dup counts + redeploy CTA */}
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-4 flex items-center justify-between gap-4 flex-wrap">
|
<CardContent className="px-6 py-4 flex items-center justify-between gap-4 flex-wrap">
|
||||||
<div className="flex items-center gap-3 flex-wrap text-sm">
|
<div className="flex items-center gap-3 flex-wrap text-sm">
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
Coolify app: <code dir="ltr" className="ms-1">{data.coolify_app_uuid.slice(0, 8)}…</code>
|
Coolify app: <code dir="ltr" className="ms-1">{data.coolify_app_uuid.slice(0, 8)}…</code>
|
||||||
</Badge>
|
</Badge>
|
||||||
{driftCount > 0 && (
|
{driftCount > 0 && (
|
||||||
<Badge variant="outline" className="text-warn border-warn/40">
|
<span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
{driftCount} drift
|
{driftCount} ב-Drift
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
{duplicatesCount > 0 && (
|
{duplicatesCount > 0 && (
|
||||||
<Badge variant="outline" className="text-warn border-warn/40">
|
<span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
{duplicatesCount} duplicates
|
{duplicatesCount} duplicates
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
{data.errors.length > 0 && (
|
{data.errors.length > 0 && (
|
||||||
<Badge variant="outline" className="text-danger border-danger/40">
|
<span className="inline-flex items-center rounded-full bg-danger-bg text-danger text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
{data.errors.join(", ")}
|
{data.errors.join(", ")}
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRedeploy}
|
onClick={handleRedeploy}
|
||||||
disabled={redeploy.isPending}
|
disabled={redeploy.isPending}
|
||||||
variant={pendingRedeploy ? "default" : "outline"}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className={pendingRedeploy ? "bg-gold text-white hover:bg-gold-deep border-transparent" : ""}
|
||||||
|
variant={pendingRedeploy ? "default" : "outline"}
|
||||||
>
|
>
|
||||||
<RefreshCw className={redeploy.isPending ? "w-3.5 h-3.5 animate-spin" : "w-3.5 h-3.5"} data-icon="inline-start" />
|
<RefreshCw className={redeploy.isPending ? "w-3.5 h-3.5 animate-spin" : "w-3.5 h-3.5"} data-icon="inline-start" />
|
||||||
{redeploy.isPending ? "Redeploying..." : "Redeploy now"}
|
{redeploy.isPending ? "פורס מחדש..." : "פרוס מחדש"}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -110,28 +112,41 @@ export function EnvironmentTab() {
|
|||||||
{CATEGORY_ORDER.map((cat) => {
|
{CATEGORY_ORDER.map((cat) => {
|
||||||
const vars = grouped.get(cat);
|
const vars = grouped.get(cat);
|
||||||
if (!vars || vars.length === 0) return null;
|
if (!vars || vars.length === 0) return null;
|
||||||
|
const catDrift = vars.filter((v) => v.drift).length;
|
||||||
return (
|
return (
|
||||||
<Card key={cat} className="bg-surface border-rule">
|
<div
|
||||||
<CardContent className="px-6 py-5">
|
key={cat}
|
||||||
<h2 className="text-navy text-lg mb-4 flex items-center gap-2">
|
className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden"
|
||||||
{CATEGORY_LABELS[cat]}
|
>
|
||||||
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
|
{/* panel header (.ph) — parchment band: title + count + drift chip */}
|
||||||
{vars.length}
|
<div className="flex items-center justify-between gap-3 px-5 py-4 border-b border-rule bg-parchment">
|
||||||
</Badge>
|
<div>
|
||||||
</h2>
|
<h2 className="text-navy text-base font-semibold mb-0 flex items-center gap-2">
|
||||||
<div className="space-y-3">
|
{CATEGORY_LABELS[cat]}
|
||||||
{vars.map((v) => (
|
<span className="text-[0.72rem] text-ink-muted font-medium tabular-nums">
|
||||||
<EnvVarRow
|
({vars.length})
|
||||||
key={v.key}
|
</span>
|
||||||
spec={v}
|
</h2>
|
||||||
coolifyAppUuid={data.coolify_app_uuid}
|
<p className="text-[0.78rem] text-ink-muted mt-0.5">
|
||||||
coolifyAvailable={coolifyAvailable}
|
מקור-האמת הוא Coolify. שינוי נכנס לתוקף רק לאחר redeploy של הקונטיינר.
|
||||||
onPendingRedeploy={() => setPendingRedeploy(true)}
|
</p>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
{coolifyAvailable && catDrift > 0 && (
|
||||||
</Card>
|
<span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5 shrink-0">
|
||||||
|
{catDrift} ב-Drift
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{vars.map((v) => (
|
||||||
|
<EnvVarRow
|
||||||
|
key={v.key}
|
||||||
|
spec={v}
|
||||||
|
coolifyAppUuid={data.coolify_app_uuid}
|
||||||
|
coolifyAvailable={coolifyAvailable}
|
||||||
|
onPendingRedeploy={() => setPendingRedeploy(true)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,60 +11,92 @@ import { RegistrationsTab } from "./_components/registrations-tab";
|
|||||||
import { BlocksTab } from "./_components/blocks-tab";
|
import { BlocksTab } from "./_components/blocks-tab";
|
||||||
import { AgentsTab } from "./_components/agents-tab";
|
import { AgentsTab } from "./_components/agents-tab";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Settings — IA-redesign composition (mockup 15): a header band with a
|
||||||
|
* top-end "redeploy" deep-link, then a two-column layout — a vertical
|
||||||
|
* sidenav (section list) + the active section panel. Implemented on top of
|
||||||
|
* shadcn Tabs so all six sections and their logic-heavy contents
|
||||||
|
* (Paperclip, agents, env vars w/ drift+redeploy, tools, blocks,
|
||||||
|
* registrations) are preserved verbatim; only the chrome is restyled from
|
||||||
|
* a horizontal tab strip to the approved sidenav layout.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const COOLIFY_APP_UUID = "gyjo0mtw2c42ej3xxvbz8zio";
|
||||||
|
const COOLIFY_REDEPLOY_URL = `https://coolify.nautilus.marcusgroup.org/project/applications/${COOLIFY_APP_UUID}`;
|
||||||
|
|
||||||
|
const SECTIONS = [
|
||||||
|
{ value: "paperclip", label: "Paperclip / סוכנים", icon: Building2 },
|
||||||
|
{ value: "agents", label: "סוכנים", icon: Bot },
|
||||||
|
{ value: "environment", label: "משתני סביבה", icon: Server },
|
||||||
|
{ value: "tools", label: "כלי MCP", icon: Wrench },
|
||||||
|
{ value: "blocks", label: "בלוקים", icon: Layers },
|
||||||
|
{ value: "registrations", label: "רישומים", icon: Plug },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<header>
|
<header className="flex items-end justify-between gap-4 flex-wrap">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<div>
|
||||||
<Link href="/" className="hover:text-gold-deep">
|
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||||
בית
|
<Link href="/" className="hover:text-gold-deep">
|
||||||
</Link>
|
בית
|
||||||
<span aria-hidden> · </span>
|
</Link>
|
||||||
<span className="text-navy">הגדרות</span>
|
<span aria-hidden> · </span>
|
||||||
</nav>
|
<span className="text-navy">הגדרות</span>
|
||||||
<h1 className="text-navy mb-0">הגדרות</h1>
|
</nav>
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
<h1 className="text-navy mb-0">הגדרות</h1>
|
||||||
תצורת המערכת, MCP server, ו-Paperclip integration.
|
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||||
</p>
|
תצורת הפלטפורמה — סוכנים, סביבה, כלים ובלוקים.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={COOLIFY_REDEPLOY_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="shrink-0 rounded-lg bg-gold text-white hover:bg-gold-deep px-5 py-2.5 text-sm font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
פרוס מחדש (redeploy)
|
||||||
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|
||||||
<Tabs dir="rtl" defaultValue="paperclip" className="space-y-4">
|
<Tabs
|
||||||
<TabsList>
|
dir="rtl"
|
||||||
<TabsTrigger value="paperclip">
|
defaultValue="environment"
|
||||||
<Building2 className="w-4 h-4" data-icon="inline-start" />
|
orientation="vertical"
|
||||||
Paperclip
|
className="flex-row gap-6 items-start"
|
||||||
</TabsTrigger>
|
>
|
||||||
<TabsTrigger value="agents">
|
{/* vertical sidenav (mockup .sidenav) */}
|
||||||
<Bot className="w-4 h-4" data-icon="inline-start" />
|
<TabsList
|
||||||
סוכנים
|
variant="line"
|
||||||
</TabsTrigger>
|
className="w-[230px] shrink-0 items-stretch gap-0 rounded-lg border border-rule bg-surface shadow-sm overflow-hidden p-0"
|
||||||
<TabsTrigger value="environment">
|
>
|
||||||
<Server className="w-4 h-4" data-icon="inline-start" />
|
{SECTIONS.map((s) => {
|
||||||
סביבה
|
const Icon = s.icon;
|
||||||
</TabsTrigger>
|
return (
|
||||||
<TabsTrigger value="tools">
|
<TabsTrigger
|
||||||
<Wrench className="w-4 h-4" data-icon="inline-start" />
|
key={s.value}
|
||||||
כלים
|
value={s.value}
|
||||||
</TabsTrigger>
|
className="relative justify-start gap-2 rounded-none border-b border-rule-soft last:border-b-0 px-4 py-3 text-sm font-medium text-ink-soft data-active:bg-gold-wash data-active:text-gold-deep data-active:font-semibold data-active:after:opacity-100 data-active:after:bg-gold hover:bg-gold-wash/40"
|
||||||
<TabsTrigger value="blocks">
|
>
|
||||||
<Layers className="w-4 h-4" data-icon="inline-start" />
|
<Icon className="w-4 h-4 shrink-0" />
|
||||||
בלוקים
|
{s.label}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="registrations">
|
);
|
||||||
<Plug className="w-4 h-4" data-icon="inline-start" />
|
})}
|
||||||
רישומים
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="paperclip"><PaperclipTab /></TabsContent>
|
<div className="flex-1 min-w-0">
|
||||||
<TabsContent value="agents"><AgentsTab /></TabsContent>
|
<TabsContent value="paperclip" className="mt-0"><PaperclipTab /></TabsContent>
|
||||||
<TabsContent value="environment"><EnvironmentTab /></TabsContent>
|
<TabsContent value="agents" className="mt-0"><AgentsTab /></TabsContent>
|
||||||
<TabsContent value="tools"><ToolsTab /></TabsContent>
|
<TabsContent value="environment" className="mt-0"><EnvironmentTab /></TabsContent>
|
||||||
<TabsContent value="blocks"><BlocksTab /></TabsContent>
|
<TabsContent value="tools" className="mt-0"><ToolsTab /></TabsContent>
|
||||||
<TabsContent value="registrations"><RegistrationsTab /></TabsContent>
|
<TabsContent value="blocks" className="mt-0"><BlocksTab /></TabsContent>
|
||||||
|
<TabsContent value="registrations" className="mt-0"><RegistrationsTab /></TabsContent>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,95 +1,64 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Plug, HardDrive, Database, FileText } from "lucide-react";
|
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
import { useSkills, type Skill } from "@/lib/api/skills";
|
import { useSkills, type Skill } from "@/lib/api/skills";
|
||||||
|
|
||||||
function formatSize(bytes: number | null) {
|
function formatChars(skill: Skill): string {
|
||||||
if (bytes == null) return "—";
|
// Mockup column = "גודל (תווים)" — the DB markdown char count, grouped.
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
const n = skill.db_markdown_chars ?? 0;
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
return n.toLocaleString("en-US");
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusDot({ tone }: { tone: string }) {
|
function formatUpdated(iso: string | null): string {
|
||||||
return <span className={`h-1.5 w-1.5 rounded-full ${tone}`} aria-hidden />;
|
if (!iso) return "—";
|
||||||
|
try {
|
||||||
|
// ISO-style date to match the mockup (2026-06-09), tabular.
|
||||||
|
return new Date(iso).toISOString().slice(0, 10);
|
||||||
|
} catch {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusBadge(s: Skill) {
|
/**
|
||||||
if (s.not_in_db) {
|
* Sync chip — colored dot + label, faithful to mockup 14:
|
||||||
return (
|
* - מסונכרן (success) when present in DB + on disk
|
||||||
<Badge variant="outline" className="gap-1.5 bg-warn-bg text-warn border-warn/40">
|
* - DB בלבד (info) when in DB but no disk copy
|
||||||
<StatusDot tone="bg-warn" />לא סונכרן
|
* - לא מסונכרן (warn) when missing from the DB
|
||||||
</Badge>
|
*/
|
||||||
);
|
function SyncChip({ skill }: { skill: Skill }) {
|
||||||
|
let tone: { wrap: string; dot: string };
|
||||||
|
let label: string;
|
||||||
|
if (skill.not_in_db) {
|
||||||
|
tone = { wrap: "bg-warn-bg text-warn", dot: "bg-warn" };
|
||||||
|
label = "לא מסונכרן";
|
||||||
|
} else if (skill.db_markdown_chars > 0 && skill.disk_exists) {
|
||||||
|
tone = { wrap: "bg-success-bg text-success", dot: "bg-success" };
|
||||||
|
label = "מסונכרן";
|
||||||
|
} else if (skill.db_markdown_chars > 0) {
|
||||||
|
tone = { wrap: "bg-info-bg text-info", dot: "bg-info" };
|
||||||
|
label = "DB בלבד";
|
||||||
|
} else {
|
||||||
|
tone = { wrap: "bg-rule-soft text-ink-muted", dot: "bg-ink-muted" };
|
||||||
|
label = "לא ידוע";
|
||||||
}
|
}
|
||||||
if (s.db_markdown_chars > 0 && s.disk_exists) {
|
|
||||||
return (
|
|
||||||
<Badge variant="outline" className="gap-1.5 bg-success-bg text-success border-success/40">
|
|
||||||
<StatusDot tone="bg-success" />מסונכרן
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (s.db_markdown_chars > 0) {
|
|
||||||
return (
|
|
||||||
<Badge variant="outline" className="gap-1.5 bg-info-bg text-info border-info/40">
|
|
||||||
<StatusDot tone="bg-info" />DB בלבד
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Badge variant="outline">לא ידוע</Badge>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SkillCard({ skill }: { skill: Skill }) {
|
|
||||||
const fileCount = skill.file_inventory?.length ?? 0;
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm hover:shadow-md transition-shadow">
|
<span
|
||||||
<CardContent className="px-5 py-4">
|
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[0.75rem] font-semibold ${tone.wrap}`}
|
||||||
<div className="flex items-start justify-between gap-3 mb-2">
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<span className={`h-1.5 w-1.5 rounded-full ${tone.dot}`} aria-hidden />
|
||||||
<Plug className="w-4 h-4 text-gold-deep shrink-0" />
|
{label}
|
||||||
<div className="min-w-0">
|
</span>
|
||||||
<h3 className="text-navy font-semibold text-base mb-0 truncate">
|
|
||||||
{skill.name || skill.slug}
|
|
||||||
</h3>
|
|
||||||
<code className="text-[0.72rem] text-ink-muted tabular-nums">
|
|
||||||
{skill.slug}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{statusBadge(skill)}
|
|
||||||
</div>
|
|
||||||
<dl className="grid grid-cols-3 gap-2 text-[0.72rem] text-ink-muted mt-3">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<FileText className="w-3 h-3" />
|
|
||||||
<span className="tabular-nums">{fileCount}</span>
|
|
||||||
<span>קבצים</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Database className="w-3 h-3" />
|
|
||||||
<span className="tabular-nums">
|
|
||||||
{(skill.db_markdown_chars / 1000).toFixed(1)}K
|
|
||||||
</span>
|
|
||||||
<span>תווים</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<HardDrive className="w-3 h-3" />
|
|
||||||
<span className="tabular-nums">
|
|
||||||
{formatSize(skill.disk_skill_md_bytes)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
{skill.updated_at && (
|
|
||||||
<p className="text-[0.7rem] text-ink-light mt-2">
|
|
||||||
עודכן: {new Date(skill.updated_at).toLocaleDateString("he-IL")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,9 +74,9 @@ export default function SkillsPage() {
|
|||||||
<span aria-hidden> · </span>
|
<span aria-hidden> · </span>
|
||||||
<span className="text-navy">מיומנויות</span>
|
<span className="text-navy">מיומנויות</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 className="text-navy mb-0">מיומנויות Paperclip</h1>
|
<h1 className="text-navy mb-0">מיומנויות</h1>
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||||
רשימת ה-skills המותקנים במערכת Paperclip ומצב הסנכרון שלהם בין ה-DB
|
סקילים מותקנים בפלטפורמה — שם, גודל, מועד עדכון ומצב הסנכרון בין ה-DB
|
||||||
לדיסק.
|
לדיסק.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
@@ -115,28 +84,69 @@ export default function SkillsPage() {
|
|||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<Card className="bg-danger-bg border-danger/40">
|
<Card className="bg-danger-bg border-danger/40 px-6 py-6 text-center text-danger">
|
||||||
<CardContent className="px-6 py-6 text-center text-danger">
|
{error.message}
|
||||||
{error.message}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
) : isPending ? (
|
) : isPending ? (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<Skeleton className="h-80 w-full rounded-lg" />
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<Skeleton key={i} className="h-32 w-full rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : data?.length === 0 ? (
|
) : data?.length === 0 ? (
|
||||||
<Card className="bg-surface border-rule">
|
<Card className="bg-surface border-rule px-6 py-12 text-center text-ink-muted">
|
||||||
<CardContent className="px-6 py-12 text-center text-ink-muted">
|
<div className="text-gold text-3xl mb-2" aria-hidden>❦</div>
|
||||||
<div className="text-gold text-3xl mb-2" aria-hidden>❦</div>
|
אין skills מותקנים
|
||||||
אין skills מותקנים
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||||
{data?.map((s) => <SkillCard key={s.slug} skill={s} />)}
|
<Table>
|
||||||
</div>
|
<TableHeader>
|
||||||
|
<TableRow className="bg-parchment hover:bg-parchment border-rule">
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
סלאג
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
שם תצוגה
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
גודל (תווים)
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
עודכן
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||||
|
סנכרון
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((s) => (
|
||||||
|
<TableRow
|
||||||
|
key={s.slug}
|
||||||
|
className="border-rule-soft hover:bg-gold-wash"
|
||||||
|
>
|
||||||
|
<TableCell className="px-5 py-4">
|
||||||
|
<code
|
||||||
|
className="font-mono text-[0.81rem] font-semibold text-navy"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{s.slug}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-5 py-4 font-semibold text-navy text-[0.9rem]">
|
||||||
|
{s.name || s.slug}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-5 py-4 text-ink-soft tabular-nums">
|
||||||
|
{formatChars(s)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-5 py-4 text-ink-muted text-[0.81rem] tabular-nums">
|
||||||
|
{formatUpdated(s.updated_at)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-5 py-4">
|
||||||
|
<SyncChip skill={s} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -22,16 +22,19 @@ export default function TrainingPage() {
|
|||||||
<AppShell>
|
<AppShell>
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<header className="flex items-start justify-between gap-4 flex-wrap">
|
<header className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<span aria-hidden> · </span>
|
<span aria-hidden> · </span>
|
||||||
<span className="text-navy">אימון סגנון</span>
|
<span className="text-navy">אימון סגנון</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 className="text-navy mb-0">הפורטרט הסגנוני של דפנה</h1>
|
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
||||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
רכישת-הסגנון של דפנה · פורטרט-הקול
|
||||||
לוח בקרה של קורפוס האימון — סטטיסטיקות, אנטומיית החלטה ממוצעת,
|
</div>
|
||||||
ביטויי חתימה, וכלי השוואה בין שתי החלטות.
|
<h1 className="text-navy mb-0">אימון סגנון</h1>
|
||||||
|
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
|
||||||
|
פורטרט הקול שנלמד מהקורפוס, מוזן read-only לכותב — סטטיסטיקות,
|
||||||
|
אנטומיית החלטה ממוצעת, ביטויי חתימה, וכלי השוואה בין החלטות.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { StatusBadge } from "@/components/cases/status-badge";
|
import { StatusBadge } from "@/components/cases/status-badge";
|
||||||
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
||||||
@@ -25,87 +25,129 @@ function formatDate(iso?: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CaseHeader({ data }: { data?: CaseDetail }) {
|
function partiesLine(data?: CaseDetail): string | null {
|
||||||
|
const appellant = data?.appellants?.filter(Boolean) ?? [];
|
||||||
|
const respondent = data?.respondents?.filter(Boolean) ?? [];
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (appellant.length) parts.push(`עוררת: ${appellant.join(", ")}`);
|
||||||
|
if (respondent.length) parts.push(`משיבה: ${respondent.join(", ")}`);
|
||||||
|
return parts.length ? parts.join(" · ") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case header — parchment band (IA-redesign mockup 17): full-bleed band with
|
||||||
|
* the case title + status/type chips inline, a parties line, the case actions
|
||||||
|
* (edit / archive / repo / sync), and a metadata strip. The `tabs` slot renders
|
||||||
|
* the tab strip inside the band, anchored to its bottom edge.
|
||||||
|
*/
|
||||||
|
export function CaseHeader({
|
||||||
|
data,
|
||||||
|
actions,
|
||||||
|
tabs,
|
||||||
|
}: {
|
||||||
|
data?: CaseDetail;
|
||||||
|
actions?: ReactNode;
|
||||||
|
tabs?: ReactNode;
|
||||||
|
}) {
|
||||||
|
const parties = partiesLine(data);
|
||||||
|
const isBlam =
|
||||||
|
data?.proceeding_type === 'בל"מ' || isBlamSubtype(data?.appeal_subtype);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6">
|
||||||
<CardContent className="px-6 py-5">
|
<nav className="text-[0.78rem] text-ink-muted mb-3 flex items-center gap-2">
|
||||||
<nav className="text-[0.78rem] text-ink-muted mb-3 flex items-center gap-2">
|
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
<span aria-hidden>·</span>
|
||||||
<span aria-hidden>·</span>
|
<span>תיקי ערר</span>
|
||||||
<span>תיקי ערר</span>
|
<span aria-hidden>·</span>
|
||||||
<span aria-hidden>·</span>
|
<span className="text-navy tabular-nums">{data?.case_number ?? "…"}</span>
|
||||||
<span className="text-navy tabular-nums">{data?.case_number ?? "…"}</span>
|
</nav>
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="flex items-start justify-between gap-6 flex-wrap">
|
<div className="flex items-start justify-between gap-6 flex-wrap">
|
||||||
<div className="space-y-2">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
{/* title row — H1 + status/type/blam chips inline (mockup .band h1) */}
|
||||||
<span className="font-display text-[2rem] font-black text-navy leading-none tabular-nums">
|
<h1 className="text-navy text-[1.7rem] font-bold leading-tight flex items-center gap-3 flex-wrap mb-0">
|
||||||
{data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"}
|
<span className="tabular-nums">
|
||||||
</span>
|
{data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"}
|
||||||
{data?.status && <StatusBadge status={data.status} />}
|
</span>
|
||||||
{data?.archived_at && (
|
{data?.status && <StatusBadge status={data.status} />}
|
||||||
<Badge
|
{data?.archived_at && (
|
||||||
variant="outline"
|
<Badge
|
||||||
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium bg-ink-muted/10 text-ink-muted border-ink-muted/30"
|
variant="outline"
|
||||||
>
|
className="rounded-full px-3 py-0.5 text-[0.75rem] font-semibold bg-ink-muted/10 text-ink-muted border-ink-muted/30"
|
||||||
בארכיון
|
>
|
||||||
</Badge>
|
בארכיון
|
||||||
)}
|
</Badge>
|
||||||
{data?.practice_area && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium bg-gold-wash text-gold-deep border-gold/40"
|
|
||||||
>
|
|
||||||
{PRACTICE_AREA_LABELS[data.practice_area]}
|
|
||||||
{data.appeal_subtype && data.appeal_subtype !== "unknown" && (
|
|
||||||
<> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]}</>
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{(data?.proceeding_type === 'בל"מ' || isBlamSubtype(data?.appeal_subtype)) && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-bold bg-warn/10 text-warn-deep border-warn/40"
|
|
||||||
title="בקשה להארכת מועד להגשת ערר"
|
|
||||||
>
|
|
||||||
בל"מ
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{data?.case_number && (
|
|
||||||
<CaseArchiveAction
|
|
||||||
caseNumber={data.case_number}
|
|
||||||
archivedAt={data.archived_at}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CreateRepoButton data={data} />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-navy text-xl font-bold leading-snug max-w-2xl mb-0">
|
|
||||||
{data?.title ?? "טוען…"}
|
|
||||||
</h1>
|
|
||||||
{data?.subject && (
|
|
||||||
<p className="text-ink-muted text-sm max-w-2xl leading-relaxed">
|
|
||||||
{data.subject}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{data?.practice_area && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-full px-3 py-0.5 text-[0.75rem] font-semibold bg-gold-wash text-gold-deep border-rule"
|
||||||
|
>
|
||||||
|
{PRACTICE_AREA_LABELS[data.practice_area]}
|
||||||
|
{data.appeal_subtype && data.appeal_subtype !== "unknown" && (
|
||||||
|
<> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]}</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isBlam && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-full px-3 py-0.5 text-[0.75rem] font-bold bg-warn/10 text-warn-deep border-warn/40"
|
||||||
|
title="בקשה להארכת מועד להגשת ערר"
|
||||||
|
>
|
||||||
|
בל"מ
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
{/* case title / subject under the heading */}
|
||||||
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
{data?.title && (
|
||||||
תאריך דיון
|
<p className="text-navy/90 text-base font-semibold mt-2 max-w-3xl leading-snug">
|
||||||
</dt>
|
{data.title}
|
||||||
<dd className="text-ink-soft tabular-nums">{formatDate(data?.hearing_date)}</dd>
|
</p>
|
||||||
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
)}
|
||||||
עודכן
|
{/* parties line (mockup .parties) */}
|
||||||
</dt>
|
{parties ? (
|
||||||
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd>
|
<p className="text-ink-soft text-sm mt-1.5">{parties}</p>
|
||||||
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
) : data?.subject ? (
|
||||||
סנכרון
|
<p className="text-ink-soft text-sm mt-1.5 max-w-3xl leading-relaxed">
|
||||||
</dt>
|
{data.subject}
|
||||||
<dd><SyncIndicator caseNumber={data?.case_number} /></dd>
|
</p>
|
||||||
</dl>
|
) : null}
|
||||||
|
|
||||||
|
{/* case actions — kept verbatim, moved into the band */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap mt-3">
|
||||||
|
{data?.case_number && (
|
||||||
|
<CaseArchiveAction
|
||||||
|
caseNumber={data.case_number}
|
||||||
|
archivedAt={data.archived_at}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CreateRepoButton data={data} />
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
{/* metadata strip — hearing date / updated / sync */}
|
||||||
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm shrink-0">
|
||||||
|
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
||||||
|
תאריך דיון
|
||||||
|
</dt>
|
||||||
|
<dd className="text-ink-soft tabular-nums">{formatDate(data?.hearing_date)}</dd>
|
||||||
|
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
||||||
|
עודכן
|
||||||
|
</dt>
|
||||||
|
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd>
|
||||||
|
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
||||||
|
סנכרון
|
||||||
|
</dt>
|
||||||
|
<dd><SyncIndicator caseNumber={data?.case_number} /></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* tab strip anchored to band bottom (mockup .tabs) */}
|
||||||
|
{tabs ? <div className="mt-5">{tabs}</div> : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import {
|
import {
|
||||||
@@ -295,6 +295,25 @@ function DocumentRow({
|
|||||||
|
|
||||||
/* ── Main panel ────────────────────────────────────────────────── */
|
/* ── Main panel ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
// IA-redesign mockup 17 — card with a parchment header band wrapping the
|
||||||
|
// (unchanged) document list. Module-level so it isn't re-created during render
|
||||||
|
// (React Compiler: "Cannot create components during render").
|
||||||
|
function DocumentsShell({ count, children }: { count: number; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||||||
|
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
|
||||||
|
מסמכי התיק
|
||||||
|
{count > 0 && (
|
||||||
|
<span className="ms-2 text-[0.72rem] text-ink-muted font-medium tabular-nums">
|
||||||
|
({count})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="px-5 py-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DocumentsPanel({
|
export function DocumentsPanel({
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
@@ -305,10 +324,12 @@ export function DocumentsPanel({
|
|||||||
|
|
||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12 text-ink-muted">
|
<DocumentsShell count={docs.length}>
|
||||||
<div className="text-gold text-2xl mb-2" aria-hidden="true">❦</div>
|
<div className="text-center py-12 text-ink-muted">
|
||||||
<p className="text-sm">אין מסמכים בתיק זה</p>
|
<div className="text-gold text-2xl mb-2" aria-hidden="true">❦</div>
|
||||||
</div>
|
<p className="text-sm">אין מסמכים בתיק זה</p>
|
||||||
|
</div>
|
||||||
|
</DocumentsShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +349,8 @@ export function DocumentsPanel({
|
|||||||
const pct = docs.length > 0 ? Math.round((done / docs.length) * 100) : 0;
|
const pct = docs.length > 0 ? Math.round((done / docs.length) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<DocumentsShell count={docs.length}>
|
||||||
|
<div className="space-y-3">
|
||||||
{hasIncomplete && (
|
{hasIncomplete && (
|
||||||
<div className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2" dir="rtl">
|
<div className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2" dir="rtl">
|
||||||
<div className="flex items-center gap-4 text-[0.78rem] flex-wrap">
|
<div className="flex items-center gap-4 text-[0.78rem] flex-wrap">
|
||||||
@@ -371,6 +393,7 @@ export function DocumentsPanel({
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DocumentsShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
|||||||
const currentIdx = phaseIndexOf(status);
|
const currentIdx = phaseIndexOf(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ol className="relative space-y-4">
|
<ol className="relative">
|
||||||
<div
|
|
||||||
className="absolute top-2 bottom-2 right-[11px] w-px bg-rule"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
{PHASES.map((phase, i) => {
|
{PHASES.map((phase, i) => {
|
||||||
const state =
|
const state =
|
||||||
currentIdx === -1 ? "pending"
|
currentIdx === -1 ? "pending"
|
||||||
@@ -51,9 +47,9 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
|||||||
: "pending";
|
: "pending";
|
||||||
|
|
||||||
const dotTone =
|
const dotTone =
|
||||||
state === "done" ? "bg-success border-success"
|
state === "done" ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
|
||||||
: state === "current" ? "bg-gold border-gold shadow-[0_0_0_4px_color-mix(in_oklab,var(--color-gold)_20%,transparent)]"
|
: state === "current" ? "bg-gold [box-shadow:0_0_0_1px_var(--color-gold)]"
|
||||||
: "bg-surface border-rule";
|
: "bg-rule";
|
||||||
|
|
||||||
const labelTone =
|
const labelTone =
|
||||||
state === "done" ? "text-ink-soft"
|
state === "done" ? "text-ink-soft"
|
||||||
@@ -66,34 +62,55 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
|||||||
: "text-ink-muted/50";
|
: "text-ink-muted/50";
|
||||||
|
|
||||||
const PhaseIcon = phase.icon;
|
const PhaseIcon = phase.icon;
|
||||||
const StatusIcon = status ? STATUS_ICONS[status] : null;
|
const isLast = i === PHASES.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={phase.key} className="relative flex items-start gap-3 ps-7">
|
<li
|
||||||
|
key={phase.key}
|
||||||
|
className="relative flex items-center gap-3 py-2"
|
||||||
|
>
|
||||||
|
{/* connector line below the dot (mockup .tl .line) */}
|
||||||
|
{!isLast && (
|
||||||
|
<span
|
||||||
|
className="absolute top-[26px] w-px h-[20px] bg-rule"
|
||||||
|
style={{ insetInlineStart: "5px" }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<span
|
<span
|
||||||
className={`absolute right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`}
|
className={`inline-block w-[11px] h-[11px] rounded-full border-2 border-surface shrink-0 ${dotTone}`}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex items-center gap-2 grow min-w-0">
|
||||||
<span className={`text-sm flex items-center gap-1.5 ${labelTone}`}>
|
<span className={`text-[0.84rem] flex items-center gap-1.5 ${labelTone}`}>
|
||||||
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
|
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
|
||||||
{phase.label}
|
{phase.label}
|
||||||
</span>
|
</span>
|
||||||
{state === "current" && status && (
|
<span className="ms-auto text-[0.72rem] text-ink-muted tabular-nums shrink-0">
|
||||||
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
|
{state === "current" ? "כעת" : state === "done" ? "✓" : "—"}
|
||||||
{StatusIcon && <StatusIcon className="w-3 h-3 shrink-0" />}
|
</span>
|
||||||
{STATUS_LABELS[status]}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{state === "current" && status && STATUS_DESCRIPTIONS[status] && (
|
|
||||||
<span className="text-[0.65rem] text-ink-muted leading-snug mt-0.5">
|
|
||||||
{STATUS_DESCRIPTIONS[status]}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* current micro-status detail under the active phase */}
|
||||||
|
{currentIdx !== -1 && status && (
|
||||||
|
<li className="ps-[26px] pt-1">
|
||||||
|
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
|
||||||
|
{STATUS_ICONS[status] &&
|
||||||
|
(() => {
|
||||||
|
const Icon = STATUS_ICONS[status];
|
||||||
|
return <Icon className="w-3 h-3 shrink-0" />;
|
||||||
|
})()}
|
||||||
|
{STATUS_LABELS[status]}
|
||||||
|
</span>
|
||||||
|
{STATUS_DESCRIPTIONS[status] && (
|
||||||
|
<span className="block text-[0.65rem] text-ink-muted leading-snug mt-0.5">
|
||||||
|
{STATUS_DESCRIPTIONS[status]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ol>
|
</ol>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,26 +33,24 @@ export function DigestCard({
|
|||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const linked = Boolean(digest.linked_case_law_id);
|
const linked = Boolean(digest.linked_case_law_id);
|
||||||
|
const pending = digest.extraction_status !== "completed";
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2">
|
<div className="flex flex-col rounded-lg border border-rule bg-surface shadow-sm p-4">
|
||||||
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
{/* top row — yomon-number + date at start, concept tag pushed to end */}
|
||||||
{digest.concept_tag && (
|
<div className="flex items-center gap-2.5 mb-2.5 flex-wrap">
|
||||||
<Badge className="bg-gold text-navy border-0">{digest.concept_tag}</Badge>
|
|
||||||
)}
|
|
||||||
{digest.yomon_number && (
|
{digest.yomon_number && (
|
||||||
<span className="font-mono" dir="ltr">
|
<span className="text-navy font-bold text-[0.95rem]">
|
||||||
יומון {digest.yomon_number}
|
יומון {digest.yomon_number}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{digest.digest_date && (
|
||||||
|
<span className="text-ink-muted text-[0.78rem]">· {formatDate(digest.digest_date)}</span>
|
||||||
|
)}
|
||||||
{digest.publication && digest.publication !== "כל יום" && (
|
{digest.publication && digest.publication !== "כל יום" && (
|
||||||
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
|
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
|
||||||
{digest.publication}
|
{digest.publication}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{digest.digest_date && <span>· {formatDate(digest.digest_date)}</span>}
|
|
||||||
{digest.practice_area && (
|
|
||||||
<span>· {practiceAreaLabel(digest.practice_area)}</span>
|
|
||||||
)}
|
|
||||||
{digest.digest_kind === "announcement" && (
|
{digest.digest_kind === "announcement" && (
|
||||||
<Badge variant="outline" className="bg-sky-50 text-sky-700 border-sky-300 text-[0.65rem]">
|
<Badge variant="outline" className="bg-sky-50 text-sky-700 border-sky-300 text-[0.65rem]">
|
||||||
עדכון
|
עדכון
|
||||||
@@ -63,47 +61,45 @@ export function DigestCard({
|
|||||||
מאמר
|
מאמר
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{digest.extraction_status !== "completed" && (
|
{digest.concept_tag && (
|
||||||
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
|
<span className="ms-auto rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
|
{digest.concept_tag}
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
{typeof score === "number" && (
|
{typeof score === "number" && (
|
||||||
<span className="ms-auto tabular-nums">דירוג {score.toFixed(2)}</span>
|
<span className="ms-1 tabular-nums text-ink-muted text-[0.78rem]">
|
||||||
|
דירוג {score.toFixed(2)}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* holding — the digest headline */}
|
||||||
{digest.headline_holding && (
|
{digest.headline_holding && (
|
||||||
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
|
<p className="text-ink font-medium text-[0.92rem] leading-6 mb-2.5" dir="rtl">
|
||||||
{digest.headline_holding}
|
{digest.headline_holding}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* source ruling — "מקור:" + citation, start-bordered (mockup `.ruling`) */}
|
||||||
|
<div className="border-s-2 border-rule ps-3 text-[0.8rem] text-ink-muted leading-6" dir="rtl">
|
||||||
|
<b className="text-ink-soft font-semibold">מקור:</b>{" "}
|
||||||
|
{digest.underlying_citation || "—"}
|
||||||
|
</div>
|
||||||
|
|
||||||
{digest.summary && (
|
{digest.summary && (
|
||||||
<p className="text-ink-soft text-sm leading-relaxed" dir="rtl">
|
<p className="text-ink-soft text-[0.82rem] leading-relaxed mt-2" dir="rtl">
|
||||||
{digest.summary}
|
{digest.summary}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-start gap-2 flex-wrap pt-1 border-t border-rule-soft mt-1">
|
{digest.practice_area && (
|
||||||
<span className="text-[0.72rem] text-ink-muted mt-1">פסק מקורי:</span>
|
<span className="text-ink-muted text-[0.72rem] mt-2">
|
||||||
<span className="text-[0.82rem] text-ink font-mono flex-1 min-w-[200px]" dir="rtl">
|
{practiceAreaLabel(digest.practice_area)}
|
||||||
{digest.underlying_citation || "—"}
|
|
||||||
</span>
|
</span>
|
||||||
{linked ? (
|
)}
|
||||||
<Link href={`/precedents/${digest.linked_case_law_id}`}>
|
|
||||||
<Badge className="bg-emerald-100 text-emerald-800 border-emerald-300 hover:bg-emerald-200">
|
|
||||||
מקושר לפסק ↗
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-300">
|
|
||||||
הפסק טרם בקורפוס
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{digest.subject_tags?.length > 0 && (
|
{digest.subject_tags?.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
{digest.subject_tags.map((t) => (
|
{digest.subject_tags.map((t) => (
|
||||||
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
||||||
{t}
|
{t}
|
||||||
@@ -112,7 +108,31 @@ export function DigestCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{actions && <div className="flex items-center gap-2 pt-1">{actions}</div>}
|
{/* foot — link-status chip + passive "ממתין לעיבוד" label (mockup `.foot`) */}
|
||||||
|
<div className="mt-3 pt-3 border-t border-rule-soft flex items-center gap-2 flex-wrap">
|
||||||
|
{linked ? (
|
||||||
|
<Link href={`/precedents/${digest.linked_case_law_id}`}>
|
||||||
|
<span className="rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-3 py-0.5 hover:opacity-90">
|
||||||
|
מקושר לפסיקה ↗
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="rounded-full bg-rule-soft text-ink-muted text-[0.72rem] font-semibold px-3 py-0.5">
|
||||||
|
לא מקושר
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{pending && (
|
||||||
|
/* passive status label — a dot + text, deliberately NOT a button */
|
||||||
|
<span className="ms-auto inline-flex items-center gap-1.5 text-[0.72rem] font-medium text-ink-muted">
|
||||||
|
<span className="h-[7px] w-[7px] rounded-full bg-warn/60" aria-hidden />
|
||||||
|
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actions && (
|
||||||
|
<div className="flex items-center gap-2 pt-3">{actions}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import type { PracticeArea } from "@/lib/api/precedent-library";
|
import type { PracticeArea } from "@/lib/api/precedent-library";
|
||||||
import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
|
import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
|
||||||
import { DigestCard } from "./digest-card";
|
import { DigestCard } from "./digest-card";
|
||||||
import { DigestUploadDialog } from "./digest-upload-dialog";
|
|
||||||
|
|
||||||
type LinkedFilter = "all" | "linked" | "unlinked";
|
type LinkedFilter = "all" | "linked" | "unlinked";
|
||||||
|
|
||||||
@@ -96,9 +95,6 @@ export function DigestListPanel() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="ms-auto">
|
|
||||||
<DigestUploadDialog />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
@@ -117,6 +113,8 @@ export function DigestListPanel() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-[0.78rem] text-ink-muted">{data.count} יומונים</p>
|
<p className="text-[0.78rem] text-ink-muted">{data.count} יומונים</p>
|
||||||
|
{/* two-column card grid (mockup 10 `.grid`) */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{data.items.map((d) => (
|
{data.items.map((d) => (
|
||||||
<DigestCard
|
<DigestCard
|
||||||
key={d.id}
|
key={d.id}
|
||||||
@@ -148,6 +146,7 @@ export function DigestListPanel() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
|
|||||||
* via the MCP drainer ``digest_process_pending`` — so the toast tells the
|
* via the MCP drainer ``digest_process_pending`` — so the toast tells the
|
||||||
* user the digest is queued, not yet searchable.
|
* user the digest is queued, not yet searchable.
|
||||||
*/
|
*/
|
||||||
export function DigestUploadDialog() {
|
export function DigestUploadDialog({ trigger }: { trigger?: React.ReactNode } = {}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [yomonNumber, setYomonNumber] = useState("");
|
const [yomonNumber, setYomonNumber] = useState("");
|
||||||
@@ -71,10 +71,12 @@ export function DigestUploadDialog() {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-navy text-parchment hover:bg-navy-soft">
|
{trigger ?? (
|
||||||
<Upload className="w-4 h-4 me-1" />
|
<Button className="bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||||
העלאת יומון
|
<Upload className="w-4 h-4 me-1" />
|
||||||
</Button>
|
העלאת יומון
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent dir="rtl">
|
<DialogContent dir="rtl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -140,7 +142,7 @@ export function DigestUploadDialog() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={upload.isPending}
|
disabled={upload.isPending}
|
||||||
className="bg-navy text-parchment hover:bg-navy-soft"
|
className="bg-gold text-white hover:bg-gold-deep border-transparent"
|
||||||
>
|
>
|
||||||
{upload.isPending ? "מעלה…" : "העלה"}
|
{upload.isPending ? "מעלה…" : "העלה"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -88,8 +88,11 @@ export function GraphFilterPanel({
|
|||||||
facets?: GraphFacets;
|
facets?: GraphFacets;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto">
|
<Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
|
||||||
<CardContent className="space-y-5 p-4">
|
<CardContent className="space-y-5 p-4">
|
||||||
|
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
|
||||||
|
פילטרים וסינון
|
||||||
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="graph-search" className="text-xs text-ink-muted">
|
<Label htmlFor="graph-search" className="text-xs text-ink-muted">
|
||||||
חיפוש פסיקה
|
חיפוש פסיקה
|
||||||
@@ -250,32 +253,50 @@ export function GraphFilterPanel({
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-xs text-ink-muted">סוגי נקודות</Label>
|
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
|
||||||
|
שכבות הגרף
|
||||||
|
</div>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="נקודות-נושא"
|
label="נקודות-נושא"
|
||||||
|
swatch="#a97d3a"
|
||||||
checked={controls.showTopics}
|
checked={controls.showTopics}
|
||||||
onCheckedChange={(v) => onChange({ showTopics: v })}
|
onCheckedChange={(v) => onChange({ showTopics: v })}
|
||||||
/>
|
/>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="נקודות-תחום"
|
label="נקודות-תחום"
|
||||||
|
swatch="#4a7c59"
|
||||||
checked={controls.showPracticeAreas}
|
checked={controls.showPracticeAreas}
|
||||||
onCheckedChange={(v) => onChange({ showPracticeAreas: v })}
|
onCheckedChange={(v) => onChange({ showPracticeAreas: v })}
|
||||||
/>
|
/>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="חוסרי מחקר (פסיקה חסרה)"
|
label="חוסרי מחקר (פסיקה חסרה)"
|
||||||
|
swatch="#a54242"
|
||||||
checked={controls.showGaps}
|
checked={controls.showGaps}
|
||||||
onCheckedChange={(v) => onChange({ showGaps: v })}
|
onCheckedChange={(v) => onChange({ showGaps: v })}
|
||||||
/>
|
/>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="יומונים (כל יום)"
|
label="יומונים (כל יום)"
|
||||||
|
swatch="#b8894a"
|
||||||
checked={controls.showDigests}
|
checked={controls.showDigests}
|
||||||
onCheckedChange={(v) => onChange({ showDigests: v })}
|
onCheckedChange={(v) => onChange({ showDigests: v })}
|
||||||
/>
|
/>
|
||||||
<ToggleRow
|
</div>
|
||||||
label="הלכות"
|
|
||||||
checked={controls.showHalachot}
|
{/* Stage-2 gate — halacha layer is dense, gated by default (mockup 11) */}
|
||||||
onCheckedChange={(v) => onChange({ showHalachot: v })}
|
<div className="rounded-lg border border-gold bg-gold-wash p-3.5 space-y-2">
|
||||||
/>
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="text-sm font-semibold text-navy m-0">שלב ב׳ — שכבת הלכות</h4>
|
||||||
|
<Switch
|
||||||
|
className="ms-auto"
|
||||||
|
checked={controls.showHalachot}
|
||||||
|
onCheckedChange={(v) => onChange({ showHalachot: v })}
|
||||||
|
aria-label="הצגת שכבת ההלכות"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-[0.72rem] text-ink-muted leading-relaxed m-0">
|
||||||
|
הפעלת שכבת ההלכות (1,454 צמתים). מגודרת כברירת-מחדל בשל הצפיפות —
|
||||||
|
הדלקה מציגה את הקשרים הלכה←פסיקה.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -350,18 +371,28 @@ function ToggleRow({
|
|||||||
checked,
|
checked,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
disabled,
|
disabled,
|
||||||
|
swatch,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onCheckedChange: (v: boolean) => void;
|
onCheckedChange: (v: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
swatch?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className={`flex items-center gap-2.5 ${disabled ? "opacity-55" : ""}`}>
|
||||||
<span className={`text-sm ${disabled ? "text-ink-muted/50" : "text-ink"}`}>
|
{swatch ? (
|
||||||
|
<span
|
||||||
|
className="inline-block size-2.5 rounded-full shrink-0 ring-1 ring-black/10"
|
||||||
|
style={{ backgroundColor: swatch }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className={`text-sm ${disabled ? "text-ink-muted/50" : "text-ink-soft"}`}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
|
className="ms-auto"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onCheckedChange={onCheckedChange}
|
onCheckedChange={onCheckedChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export function GraphView() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between gap-3 text-xs text-ink-muted">
|
<div className="flex items-center justify-between gap-3 text-xs text-ink-muted">
|
||||||
<span>
|
<span className="inline-flex items-center gap-2 rounded-full border border-rule bg-surface px-3 py-1 tabular-nums">
|
||||||
{data ? `${data.nodes.length} נקודות · ${data.edges.length} קשרים` : "—"}
|
{data ? `${data.nodes.length} נקודות · ${data.edges.length} קשרים` : "—"}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -248,12 +248,12 @@ export function GraphView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4 h-[calc(100vh-320px)] min-h-[560px]">
|
<div className="flex gap-5 h-[calc(100vh-320px)] min-h-[560px] items-start">
|
||||||
<GraphFilterPanel controls={controls} onChange={onChange} facets={facets} />
|
<GraphFilterPanel controls={controls} onChange={onChange} facets={facets} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={canvasAreaRef}
|
ref={canvasAreaRef}
|
||||||
className="relative flex-1 rounded-lg border border-rule bg-surface overflow-hidden"
|
className="relative flex-1 h-full rounded-lg border border-rule bg-gradient-to-b from-[#f3ecda] to-[#efe6cf] shadow-sm overflow-hidden"
|
||||||
>
|
>
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="grid h-full place-items-center p-6 text-center">
|
<div className="grid h-full place-items-center p-6 text-center">
|
||||||
@@ -311,6 +311,12 @@ export function GraphView() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Legend colorBy={controls.colorBy} />
|
<Legend colorBy={controls.colorBy} />
|
||||||
|
|
||||||
|
{data ? (
|
||||||
|
<div className="absolute bottom-3 start-3 rounded-full bg-surface/70 backdrop-blur px-2.5 py-1 text-[0.72rem] text-ink-muted tabular-nums">
|
||||||
|
{data.nodes.length} צמתים מוצגים
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedNode ? (
|
{selectedNode ? (
|
||||||
@@ -340,9 +346,16 @@ function RankingPanel({
|
|||||||
.sort((a, b) => (b.betweenness ?? 0) - (a.betweenness ?? 0))
|
.sort((a, b) => (b.betweenness ?? 0) - (a.betweenness ?? 0))
|
||||||
.slice(0, 12);
|
.slice(0, 12);
|
||||||
|
|
||||||
|
const communities = new Set(
|
||||||
|
nodes.map((n) => n.community).filter((c) => c != null),
|
||||||
|
).size;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto">
|
<Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4 space-y-4">
|
||||||
|
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
|
||||||
|
אנליטיקה
|
||||||
|
</div>
|
||||||
<Tabs defaultValue="pagerank">
|
<Tabs defaultValue="pagerank">
|
||||||
<TabsList className="w-full">
|
<TabsList className="w-full">
|
||||||
<TabsTrigger value="pagerank" className="flex-1">
|
<TabsTrigger value="pagerank" className="flex-1">
|
||||||
@@ -359,6 +372,12 @@ function RankingPanel({
|
|||||||
<RankList items={byBetweenness} metric="betweenness" onPick={onPick} />
|
<RankList items={byBetweenness} metric="betweenness" onPick={onPick} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
{communities > 0 ? (
|
||||||
|
<div className="flex items-center gap-2 border-t border-rule-soft pt-3 text-sm text-ink-soft">
|
||||||
|
אשכולות:
|
||||||
|
<b className="text-navy text-lg tabular-nums">{communities}</b>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -377,18 +396,19 @@ function RankList({
|
|||||||
return <p className="text-ink-muted text-xs mt-3">אין נתונים.</p>;
|
return <p className="text-ink-muted text-xs mt-3">אין נתונים.</p>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ol className="mt-2 space-y-1">
|
<ol className="mt-2">
|
||||||
{items.map((n, i) => (
|
{items.map((n, i) => (
|
||||||
<li key={n.id}>
|
<li key={n.id} className="border-b border-rule-soft last:border-b-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPick(n)}
|
onClick={() => onPick(n)}
|
||||||
className="flex w-full items-baseline justify-between gap-2 rounded px-2 py-1 text-start text-sm hover:bg-gold-wash"
|
className="flex w-full items-baseline gap-2 px-1 py-1.5 text-start text-sm hover:bg-gold-wash rounded"
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="w-4 shrink-0 text-ink-muted text-xs tabular-nums">
|
||||||
<span className="text-ink-muted text-xs">{i + 1}.</span> {n.label}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-ink-muted text-xs tabular-nums shrink-0">
|
<span className="truncate text-ink-soft">{n.label}</span>
|
||||||
|
<span className="ms-auto text-gold-deep font-semibold text-xs tabular-nums shrink-0">
|
||||||
{((n[metric] ?? 0) * 100).toFixed(0)}
|
{((n[metric] ?? 0) * 100).toFixed(0)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
@@ -115,84 +115,124 @@ export function ContentChecklistsPanel() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemCount = current
|
||||||
|
? current.draft.split("\n").filter((l) => /^\s*-\s*\[/.test(l)).length
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-[18px]">
|
||||||
{/* Tab selector */}
|
{/* Type buttons — gold active (mockup 13) */}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2.5 flex-wrap">
|
||||||
{items.map((item) => (
|
{items.map((item) => {
|
||||||
<Button
|
const isActive = active === item.key;
|
||||||
key={item.key}
|
return (
|
||||||
size="sm"
|
<button
|
||||||
variant={active === item.key ? "default" : "outline"}
|
key={item.key}
|
||||||
onClick={() => { setActive(item.key); setPreview(false); }}
|
type="button"
|
||||||
className="text-xs"
|
onClick={() => {
|
||||||
>
|
setActive(item.key);
|
||||||
{item.label}
|
setPreview(false);
|
||||||
{item.isOverride && (
|
}}
|
||||||
<Badge variant="secondary" className="text-[9px] mr-1.5 px-1">
|
className={
|
||||||
מותאם
|
isActive
|
||||||
</Badge>
|
? "rounded-lg border border-gold bg-gold px-4 py-2 text-[0.84rem] font-semibold text-white"
|
||||||
)}
|
: "rounded-lg border border-rule bg-surface px-4 py-2 text-[0.84rem] font-medium text-ink-soft hover:border-gold/50"
|
||||||
</Button>
|
}
|
||||||
))}
|
>
|
||||||
|
{item.label}
|
||||||
|
{item.isOverride && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className={`text-[9px] ms-1.5 px-1 ${isActive ? "bg-white/20 text-white" : ""}`}
|
||||||
|
>
|
||||||
|
מותאם
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor / Preview */}
|
{/* "חל על:" explainer band — gold-wash (mockup 13) */}
|
||||||
|
{current && CHECKLIST_APPLIES[current.key] && (
|
||||||
|
<div className="flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5 text-[0.84rem] text-ink-soft">
|
||||||
|
<b className="text-gold-deep font-semibold whitespace-nowrap">חל על:</b>
|
||||||
|
<span>{CHECKLIST_APPLIES[current.key]}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Editor — framed card with header chip + parchment editor + footer */}
|
||||||
{current && (
|
{current && (
|
||||||
<Card className="border-rule">
|
<Card className="border-rule shadow-sm overflow-hidden p-0 gap-0">
|
||||||
<CardContent className="px-5 py-4 space-y-3">
|
<div className="flex items-center gap-2.5 px-[18px] py-3.5 border-b border-rule-soft">
|
||||||
<div className="flex items-center justify-between">
|
<h2 className="text-[0.95rem] font-semibold text-navy m-0">
|
||||||
<div className="min-w-0">
|
צ׳קליסט תוכן — {current.label}
|
||||||
<h3 className="text-sm font-semibold text-navy">{current.label}</h3>
|
</h2>
|
||||||
{CHECKLIST_APPLIES[current.key] && (
|
<span
|
||||||
<p className="text-[0.72rem] text-ink-muted mt-0.5">
|
className={`ms-auto rounded-full text-xs font-semibold px-2.5 py-0.5 ${
|
||||||
חל על: {CHECKLIST_APPLIES[current.key]}
|
current.isOverride
|
||||||
</p>
|
? "bg-gold-wash text-gold-deep border border-rule"
|
||||||
)}
|
: "bg-info-bg text-info"
|
||||||
</div>
|
}`}
|
||||||
<Button
|
>
|
||||||
size="sm"
|
{current.isOverride ? "מותאם" : "ידני"}
|
||||||
variant="ghost"
|
</span>
|
||||||
onClick={() => setPreview(!preview)}
|
<Button
|
||||||
className="text-xs shrink-0"
|
size="sm"
|
||||||
>
|
variant="ghost"
|
||||||
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />}
|
onClick={() => setPreview(!preview)}
|
||||||
{preview ? "עריכה" : "תצוגה מקדימה"}
|
className="text-xs shrink-0 h-7"
|
||||||
</Button>
|
>
|
||||||
</div>
|
{preview ? (
|
||||||
|
<EyeOff className="w-3.5 h-3.5 ms-1" />
|
||||||
{preview ? (
|
) : (
|
||||||
<div className="border border-rule rounded-md p-4 bg-sand-soft/30 max-h-[500px] overflow-y-auto">
|
<Eye className="w-3.5 h-3.5 ms-1" />
|
||||||
<Markdown content={current.draft} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Textarea
|
|
||||||
value={current.draft}
|
|
||||||
onChange={(e) => updateDraft(e.target.value)}
|
|
||||||
className="min-h-[400px] font-mono text-sm leading-relaxed"
|
|
||||||
dir="rtl"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button size="sm" disabled={!current.dirty || update.isPending} onClick={handleSave}>
|
|
||||||
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
|
|
||||||
שמור
|
|
||||||
</Button>
|
|
||||||
{current.isOverride && (
|
|
||||||
<Button size="sm" variant="outline" disabled={reset.isPending} onClick={handleReset}>
|
|
||||||
<RotateCcw className="w-3 h-3 ml-1" />
|
|
||||||
איפוס לברירת מחדל
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<Badge
|
{preview ? "עריכה" : "תצוגה מקדימה"}
|
||||||
variant={current.isOverride ? "default" : "secondary"}
|
</Button>
|
||||||
className="text-[10px] mr-auto"
|
</div>
|
||||||
>
|
|
||||||
{current.isOverride ? "מותאם" : "ברירת מחדל"}
|
{preview ? (
|
||||||
</Badge>
|
<div className="p-[18px] bg-parchment max-h-[500px] overflow-y-auto border-b border-rule-soft">
|
||||||
|
<Markdown content={current.draft} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
) : (
|
||||||
|
<Textarea
|
||||||
|
value={current.draft}
|
||||||
|
onChange={(e) => updateDraft(e.target.value)}
|
||||||
|
className="min-h-[340px] rounded-none border-0 border-b border-rule-soft bg-parchment font-mono text-[0.84rem] leading-[1.95] text-ink-soft focus-visible:ring-0 resize-y"
|
||||||
|
dir="rtl"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5 px-[18px] py-3.5">
|
||||||
|
<Button
|
||||||
|
disabled={!current.dirty || update.isPending}
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-gold text-white hover:bg-gold-deep"
|
||||||
|
>
|
||||||
|
{update.isPending ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin ms-1" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-3.5 h-3.5 ms-1" />
|
||||||
|
)}
|
||||||
|
שמור
|
||||||
|
</Button>
|
||||||
|
{current.isOverride && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={reset.isPending}
|
||||||
|
onClick={handleReset}
|
||||||
|
className="border-rule text-navy"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5 ms-1" />
|
||||||
|
אפס לברירת-מחדל
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<span className="ms-auto text-[0.78rem] text-ink-muted tabular-nums">
|
||||||
|
{itemCount} פריטים
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
useDeleteMissingPrecedent,
|
useDeleteMissingPrecedent,
|
||||||
CITED_BY_PARTY_LABELS,
|
CITED_BY_PARTY_LABELS,
|
||||||
STATUS_LABELS,
|
STATUS_LABELS,
|
||||||
|
type CitedByParty,
|
||||||
type MissingPrecedent,
|
type MissingPrecedent,
|
||||||
type MissingPrecedentStatus,
|
type MissingPrecedentStatus,
|
||||||
} from "@/lib/api/missing-precedents";
|
} from "@/lib/api/missing-precedents";
|
||||||
@@ -29,20 +30,39 @@ function formatDate(iso: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Status chip — mockup 09 tones (open=warn, uploaded=info, closed=success,
|
||||||
|
* irrelevant=muted). Pill-shaped, whitespace-nowrap. */
|
||||||
function StatusBadge({ status }: { status: MissingPrecedentStatus }) {
|
function StatusBadge({ status }: { status: MissingPrecedentStatus }) {
|
||||||
const variants: Record<MissingPrecedentStatus, string> = {
|
const variants: Record<MissingPrecedentStatus, string> = {
|
||||||
open: "bg-gold-wash text-gold-deep border-gold/40",
|
open: "bg-warn-bg text-warn border-transparent",
|
||||||
uploaded: "bg-rule-soft text-ink-muted border-rule",
|
uploaded: "bg-info-bg text-info border-transparent",
|
||||||
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60",
|
closed: "bg-success-bg text-success border-transparent",
|
||||||
irrelevant: "bg-rule-soft text-ink-muted border-rule line-through",
|
irrelevant: "bg-rule-soft text-ink-muted border-transparent",
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className={variants[status]}>
|
<Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[status]}`}>
|
||||||
{STATUS_LABELS[status]}
|
{STATUS_LABELS[status]}
|
||||||
</Badge>
|
</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 }) {
|
function TableSkeleton({ cols }: { cols: number }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -100,14 +120,14 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
|||||||
<>
|
<>
|
||||||
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-rule-soft/60">
|
<TableHeader className="bg-parchment">
|
||||||
<TableRow className="border-rule">
|
<TableRow className="border-rule hover:bg-transparent">
|
||||||
<TableHead className="text-navy text-right">פסיקה</TableHead>
|
<TableHead className="text-ink-muted text-right font-medium text-xs">פסיקה</TableHead>
|
||||||
<TableHead className="text-navy text-right">נושא</TableHead>
|
<TableHead className="text-ink-muted text-right font-medium text-xs">נושא</TableHead>
|
||||||
<TableHead className="text-navy text-right">תיק</TableHead>
|
<TableHead className="text-ink-muted text-right font-medium text-xs">תיק</TableHead>
|
||||||
<TableHead className="text-navy text-right">צד מצטט</TableHead>
|
<TableHead className="text-ink-muted text-right font-medium text-xs">צוטט ע״י</TableHead>
|
||||||
<TableHead className="text-navy text-right">סטטוס</TableHead>
|
<TableHead className="text-ink-muted text-right font-medium text-xs">סטטוס</TableHead>
|
||||||
<TableHead className="text-navy text-right">נוצר</TableHead>
|
<TableHead className="text-ink-muted text-right font-medium text-xs">נוצר</TableHead>
|
||||||
<TableHead className="text-navy" />
|
<TableHead className="text-navy" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -128,7 +148,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
|||||||
onClick={() => setOpenId(mp.id)}
|
onClick={() => setOpenId(mp.id)}
|
||||||
>
|
>
|
||||||
<TableCell className="max-w-[440px]">
|
<TableCell className="max-w-[440px]">
|
||||||
<div className="text-sm text-navy font-medium truncate">
|
<div className="text-sm text-navy font-semibold truncate">
|
||||||
{mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")}
|
{mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
||||||
@@ -153,11 +173,9 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-ink">
|
<TableCell className="text-sm text-ink">
|
||||||
{mp.cited_by_party
|
<SourceChip party={mp.cited_by_party} />
|
||||||
? CITED_BY_PARTY_LABELS[mp.cited_by_party]
|
|
||||||
: "—"}
|
|
||||||
{mp.cited_by_party_name ? (
|
{mp.cited_by_party_name ? (
|
||||||
<div className="text-[0.7rem] text-ink-muted truncate max-w-[160px]">
|
<div className="text-[0.7rem] text-ink-muted truncate max-w-[160px] mt-1">
|
||||||
{mp.cited_by_party_name}
|
{mp.cited_by_party_name}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -165,7 +183,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<StatusBadge status={mp.status} />
|
<StatusBadge status={mp.status} />
|
||||||
{mp.linked_case_law_number ? (
|
{mp.linked_case_law_number ? (
|
||||||
<div className="text-[0.7rem] text-emerald-700 mt-1">
|
<div className="text-[0.7rem] text-success mt-1">
|
||||||
↳ {mp.linked_case_law_name || mp.linked_case_law_number}
|
↳ {mp.linked_case_law_name || mp.linked_case_law_number}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -174,22 +192,39 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
|||||||
{formatDate(mp.created_at)}
|
{formatDate(mp.created_at)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-end">
|
<TableCell className="text-end">
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Button
|
{mp.status === "open" ? (
|
||||||
variant="ghost"
|
/* gold "העלה והשלם" CTA (mockup 09 `.btn`) */
|
||||||
size="sm"
|
<Button
|
||||||
onClick={(e) => {
|
size="sm"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
setOpenId(mp.id);
|
e.stopPropagation();
|
||||||
}}
|
setOpenId(mp.id);
|
||||||
title={mp.status === "open" ? "העלאה" : "פרטים"}
|
}}
|
||||||
>
|
className="h-7 bg-gold text-white hover:bg-gold-deep border-transparent text-[0.78rem] font-semibold"
|
||||||
{mp.status === "open" ? (
|
>
|
||||||
<Upload className="w-4 h-4" />
|
<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" />
|
<Pencil className="w-4 h-4" />
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -24,32 +24,56 @@ function formatDate(iso: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Score chip — boxed, gold-deep, tabular (mockup 07 `.score`). Sits at the
|
||||||
|
* end of the meta row via `ms-auto`. White fill on gold-wash halacha cards. */
|
||||||
|
function ScoreChip({ score, onWash }: { score: number; onWash?: boolean }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`ms-auto rounded-md border border-rule px-2.5 py-0.5 text-xs font-semibold text-gold-deep tabular-nums ${
|
||||||
|
onWash ? "bg-white" : "bg-surface"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
דירוג {score.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> }) {
|
function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-gold/40 bg-gold-wash/40 p-4 space-y-2">
|
<div className="rounded-lg border border-rule bg-gold-wash p-4 shadow-sm space-y-2.5">
|
||||||
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||||
<Badge className="bg-gold text-navy border-0">הלכה</Badge>
|
<Badge className="rounded bg-gold text-white border-0 text-[0.68rem] font-bold tracking-wide">
|
||||||
<span className="font-mono" dir="ltr">{hit.case_number}</span>
|
הלכה
|
||||||
{hit.court && <span>· {hit.court}</span>}
|
</Badge>
|
||||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
<span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
|
||||||
{hit.precedent_level && <span>· {hit.precedent_level}</span>}
|
{(hit.court || hit.decision_date || hit.precedent_level) && (
|
||||||
{/* PRE-3/PRE-5 (INV-IA5): the derived authority (binding/persuasive)
|
<span className="text-ink-muted">
|
||||||
rides on the wire but was dropped here — render it as in the review
|
{hit.court ? `· ${hit.court}` : ""}
|
||||||
tab so search shows the same provenance everywhere. */}
|
{hit.decision_date ? ` · ${formatDate(hit.decision_date)}` : ""}
|
||||||
|
{hit.precedent_level ? ` · ${hit.precedent_level}` : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* PRE-3/PRE-5 (INV-IA5): derived authority (binding/persuasive)
|
||||||
|
rides on the wire — render the pill as in the review tab. */}
|
||||||
<AuthorityBadge authority={hit.authority} />
|
<AuthorityBadge authority={hit.authority} />
|
||||||
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
<ScoreChip score={hit.score} onWash />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
|
<p className="text-ink font-medium text-[0.95rem] leading-7" dir="rtl">
|
||||||
{hit.rule_statement}
|
{hit.rule_statement}
|
||||||
</p>
|
</p>
|
||||||
<blockquote className="text-ink-soft text-sm border-s-2 border-gold ps-3" dir="rtl">
|
<blockquote
|
||||||
|
className="rounded-e border-s-[3px] border-gold bg-white/50 px-3 py-2 text-sm text-ink-soft leading-7"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
“{hit.supporting_quote}”
|
“{hit.supporting_quote}”
|
||||||
{hit.page_reference && <span className="text-ink-muted text-[0.72rem] ms-2">({hit.page_reference})</span>}
|
{hit.page_reference && (
|
||||||
|
<span className="text-ink-muted text-[0.72rem] ms-2">({hit.page_reference})</span>
|
||||||
|
)}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
{hit.subject_tags?.length > 0 && (
|
{hit.subject_tags?.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{hit.subject_tags.map((t) => (
|
{hit.subject_tags.map((t) => (
|
||||||
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
<Badge key={t} variant="outline" className="text-[0.65rem] bg-white">
|
||||||
{t}
|
{t}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
@@ -61,16 +85,24 @@ function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> })
|
|||||||
|
|
||||||
function PassageCard({ hit }: { hit: Extract<SearchHit, { type: "passage" }> }) {
|
function PassageCard({ hit }: { hit: Extract<SearchHit, { type: "passage" }> }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2">
|
<div className="rounded-lg border border-rule bg-surface p-4 shadow-sm space-y-2.5">
|
||||||
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||||
<Badge variant="outline" className="bg-info-bg text-info border-transparent">קטע</Badge>
|
<Badge variant="outline" className="rounded bg-info-bg text-info border-transparent text-[0.68rem] font-bold tracking-wide">
|
||||||
<span className="font-mono" dir="ltr">{hit.case_number}</span>
|
קטע
|
||||||
{hit.court && <span>· {hit.court}</span>}
|
</Badge>
|
||||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
<span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
|
||||||
<span className="text-[0.7rem]">· {hit.section_type}</span>
|
{(hit.court || hit.decision_date) && (
|
||||||
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
<span className="text-ink-muted">
|
||||||
|
{hit.court ? `· ${hit.court}` : ""}
|
||||||
|
{hit.decision_date ? ` · ${formatDate(hit.decision_date)}` : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="rounded bg-rule-soft text-ink-muted text-[0.68rem] px-2 py-0.5 font-medium">
|
||||||
|
{hit.section_type}
|
||||||
|
</span>
|
||||||
|
<ScoreChip score={hit.score} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-ink text-sm leading-relaxed" dir="rtl">
|
<p className="text-ink-soft text-sm leading-7" dir="rtl">
|
||||||
{hit.content.slice(0, 600)}
|
{hit.content.slice(0, 600)}
|
||||||
{hit.content.length > 600 && <span>…</span>}
|
{hit.content.length > 600 && <span>…</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -98,51 +130,61 @@ export function LibrarySearchPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<form onSubmit={onSubmit} className="flex items-end gap-3 flex-wrap">
|
{/* search panel — boxed surface with query row + two-up filter row +
|
||||||
<div className="flex-1 min-w-[300px]">
|
controls strip (checkbox start, gold CTA end) per mockup 07. */}
|
||||||
<label className="text-[0.78rem] text-ink-muted">שאילתת חיפוש</label>
|
<form
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className="rounded-lg border border-rule bg-surface shadow-sm p-5 space-y-3.5"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">
|
||||||
|
שאילתת חיפוש
|
||||||
|
</label>
|
||||||
<Input value={draft} onChange={(e) => setDraft(e.target.value)}
|
<Input value={draft} onChange={(e) => setDraft(e.target.value)}
|
||||||
placeholder="השבחה אובייקטיבית" dir="rtl" />
|
placeholder="השבחה אובייקטיבית" dir="rtl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-[180px]">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
|
||||||
<label className="text-[0.78rem] text-ink-muted">תחום</label>
|
<div>
|
||||||
<Select value={practiceArea || "_all"}
|
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">תחום</label>
|
||||||
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
|
<Select value={practiceArea || "_all"}
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
|
||||||
<SelectContent>
|
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
||||||
<SelectItem value="_all">הכל</SelectItem>
|
<SelectContent>
|
||||||
{PRACTICE_AREAS.map((a) => (
|
<SelectItem value="_all">הכל</SelectItem>
|
||||||
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
|
{PRACTICE_AREAS.map((a) => (
|
||||||
))}
|
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">רמת תקדים</label>
|
||||||
|
<Select value={precedentLevel || "_all"}
|
||||||
|
onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}>
|
||||||
|
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="_all">הכל</SelectItem>
|
||||||
|
{PRECEDENT_LEVELS.map((l) => (
|
||||||
|
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-[170px]">
|
<div className="flex items-center gap-4 flex-wrap pt-1">
|
||||||
<label className="text-[0.78rem] text-ink-muted">רמת תקדים</label>
|
<label className="flex items-center gap-2 cursor-pointer text-[0.85rem] text-ink-soft">
|
||||||
<Select value={precedentLevel || "_all"}
|
<input type="checkbox" className="w-[15px] h-[15px] accent-gold" checked={includeHalachot}
|
||||||
onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}>
|
onChange={(e) => setIncludeHalachot(e.target.checked)} />
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
כלול הלכות
|
||||||
<SelectContent>
|
</label>
|
||||||
<SelectItem value="_all">הכל</SelectItem>
|
<Button type="submit" className="ms-auto bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||||
{PRECEDENT_LEVELS.map((l) => (
|
<Search className="w-4 h-4 me-1" />
|
||||||
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
חפש
|
||||||
))}
|
</Button>
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" className="bg-navy text-parchment hover:bg-navy-soft">
|
|
||||||
<Search className="w-4 h-4 me-1" />
|
|
||||||
חפש
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-sm text-ink-muted">
|
|
||||||
<input type="checkbox" checked={includeHalachot}
|
|
||||||
onChange={(e) => setIncludeHalachot(e.target.checked)} />
|
|
||||||
כלול הלכות (rule-level matches)
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{!query.trim() ? (
|
{!query.trim() ? (
|
||||||
<div className="text-center text-ink-muted py-12">
|
<div className="text-center text-ink-muted py-12">
|
||||||
הקלד שאילתא כדי לחפש בקורפוס. החיפוש סמנטי — לא טקסטואלי.
|
הקלד שאילתא כדי לחפש בקורפוס. החיפוש סמנטי — לא טקסטואלי.
|
||||||
@@ -160,11 +202,13 @@ export function LibrarySearchPanel() {
|
|||||||
לא נמצאו תוצאות. נסה ניסוח אחר או הסר פילטרים.
|
לא נמצאו תוצאות. נסה ניסוח אחר או הסר פילטרים.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3.5">
|
||||||
<p className="text-[0.78rem] text-ink-muted flex items-center gap-2">
|
<p className="text-[0.82rem] text-ink-muted flex items-center gap-2">
|
||||||
<span className="inline-flex items-center rounded-full bg-success-bg text-success text-[0.7rem] font-semibold px-2.5 py-0.5">
|
<span>תוצאות</span>
|
||||||
|
<span className="inline-flex items-center rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
הלכות מאושרות בלבד
|
הלכות מאושרות בלבד
|
||||||
</span>
|
</span>
|
||||||
|
<span aria-hidden>·</span>
|
||||||
<span className="tabular-nums">{data.count} תוצאות</span>
|
<span className="tabular-nums">{data.count} תוצאות</span>
|
||||||
</p>
|
</p>
|
||||||
{data.items.map((hit, i) =>
|
{data.items.map((hit, i) =>
|
||||||
|
|||||||
@@ -128,60 +128,6 @@ function LinkDialog({ caseId, currentRelated, open, onOpenChange }: DialogProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Related Case Card ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function RelatedCaseCard({ caseId, related }: { caseId: string; related: RelatedCase }) {
|
|
||||||
const { mutateAsync: unlinkCase, isPending } = useUnlinkRelatedCase(caseId);
|
|
||||||
|
|
||||||
async function handleUnlink() {
|
|
||||||
try {
|
|
||||||
await unlinkCase(related.id);
|
|
||||||
toast.success("הקישור הוסר");
|
|
||||||
} catch {
|
|
||||||
toast.error("שגיאה בהסרת הקישור");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-3 px-3 py-2.5 rounded-lg border border-rule bg-surface">
|
|
||||||
<a
|
|
||||||
href={`/precedents/${related.id}`}
|
|
||||||
className="min-w-0 flex-1 hover:opacity-80 transition-opacity"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium text-navy truncate">
|
|
||||||
{related.case_name || related.case_number}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
|
||||||
{related.precedent_level && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-[0.62rem] ${LEVEL_COLORS[related.precedent_level] ?? ""}`}
|
|
||||||
>
|
|
||||||
{LEVEL_LABELS[related.precedent_level] ?? related.precedent_level}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{related.court && (
|
|
||||||
<span className="text-[0.7rem] text-ink-muted truncate">{related.court}</span>
|
|
||||||
)}
|
|
||||||
{related.date && (
|
|
||||||
<span className="text-[0.7rem] text-ink-muted tabular-nums" dir="ltr">
|
|
||||||
{related.date.slice(0, 10)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
onClick={handleUnlink}
|
|
||||||
disabled={isPending}
|
|
||||||
className="p-1 rounded hover:bg-danger-bg hover:text-danger transition-colors text-ink-muted disabled:opacity-40 shrink-0"
|
|
||||||
title="הסר קישור"
|
|
||||||
>
|
|
||||||
<X className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Public section component ─────────────────────────────────────────
|
// ── Public section component ─────────────────────────────────────────
|
||||||
|
|
||||||
type SectionProps = {
|
type SectionProps = {
|
||||||
@@ -189,28 +135,68 @@ type SectionProps = {
|
|||||||
related: RelatedCase[];
|
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) {
|
export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="text-navy text-sm font-semibold">
|
<h3 className="text-navy text-[0.92rem] font-semibold m-0">
|
||||||
החלטות קשורות{related.length > 0 ? ` (${related.length})` : ""}
|
ציטוטים מקושרים{related.length > 0 ? ` (${related.length})` : ""}
|
||||||
</h3>
|
</h3>
|
||||||
<Button variant="outline" size="sm" onClick={() => setDialogOpen(true)}>
|
<Button
|
||||||
<Link2 className="w-3.5 h-3.5 me-1" /> קשר החלטה
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{related.length === 0 ? (
|
{related.length === 0 ? (
|
||||||
<p className="text-ink-muted text-sm">אין החלטות קשורות עדיין</p>
|
<p className="text-ink-muted text-[0.82rem] m-0">אין החלטות קשורות עדיין</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1.5">
|
<ul className="list-none p-0 m-0">
|
||||||
{related.map((r) => (
|
{related.map((r) => (
|
||||||
<RelatedCaseCard key={r.id} caseId={caseId} related={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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LinkDialog
|
<LinkDialog
|
||||||
@@ -222,3 +208,24 @@ export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
|||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { SubjectDonut } from "@/components/training/subject-donut";
|
import { SubjectDonut } from "@/components/training/subject-donut";
|
||||||
import { useStyleReport } from "@/lib/api/training";
|
import { useStyleReport, useCuratorStats } from "@/lib/api/training";
|
||||||
|
|
||||||
|
// Mockup 12 anatomy palette — info · gold · gold-deep · success, cycling.
|
||||||
|
const ANATOMY_COLORS = ["#4e6a8c", "#a97d3a", "#8b6428", "#4a7c59"];
|
||||||
|
|
||||||
function KPICard({
|
function KPICard({
|
||||||
label,
|
label,
|
||||||
@@ -16,15 +19,13 @@ function KPICard({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-5 py-4 flex flex-col gap-0.5">
|
<CardContent className="px-[18px] py-4 flex flex-col">
|
||||||
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
|
<span className="font-display text-[1.85rem] font-bold leading-[1.1] text-navy tabular-nums">
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<span className="font-display text-[2rem] font-black leading-none text-navy">
|
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-[0.81rem] text-ink-soft mt-1">{label}</span>
|
||||||
{caption && (
|
{caption && (
|
||||||
<span className="text-[0.78rem] text-ink-muted mt-1">{caption}</span>
|
<span className="text-[0.72rem] text-ink-muted mt-0.5">{caption}</span>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -33,6 +34,7 @@ function KPICard({
|
|||||||
|
|
||||||
export function StyleReportPanel() {
|
export function StyleReportPanel() {
|
||||||
const { data, isPending, error } = useStyleReport();
|
const { data, isPending, error } = useStyleReport();
|
||||||
|
const curator = useCuratorStats();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@@ -63,14 +65,13 @@ export function StyleReportPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Headline */}
|
{/* Headline banner — gold-wash, ★ aligned to start (mockup 12) */}
|
||||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
<div className="flex items-start gap-3 rounded-lg border border-gold bg-gold-wash px-5 py-4 shadow-sm">
|
||||||
<CardContent className="px-6 py-4">
|
<span className="text-gold-deep text-xl leading-tight shrink-0">★</span>
|
||||||
<p className="font-display text-gold-deep text-lg font-semibold leading-snug">
|
<p className="text-ink-soft text-[0.95rem] leading-relaxed m-0">
|
||||||
★ {c.headline}
|
{c.headline}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* KPIs */}
|
{/* KPIs */}
|
||||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
||||||
@@ -121,23 +122,22 @@ export function StyleReportPanel() {
|
|||||||
{data.anatomy.sections.length === 0 ? (
|
{data.anatomy.sections.length === 0 ? (
|
||||||
<p className="text-ink-muted text-sm">אין נתונים על מבנה</p>
|
<p className="text-ink-muted text-sm">אין נתונים על מבנה</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2.5">
|
<ul className="space-y-3.5">
|
||||||
{data.anatomy.sections.map((s) => {
|
{data.anatomy.sections.map((s, i) => {
|
||||||
const pct = Math.round(s.pct * 100);
|
const pct = Math.round(s.pct * 100);
|
||||||
|
const fill = ANATOMY_COLORS[i % ANATOMY_COLORS.length];
|
||||||
return (
|
return (
|
||||||
<li key={s.type} className="space-y-1">
|
<li key={s.type} className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between text-[0.78rem]">
|
<div className="flex items-center justify-between text-[0.81rem]">
|
||||||
<span className="text-ink-soft font-medium">
|
<span className="text-ink-soft">{s.label}</span>
|
||||||
{s.label}
|
<span className="text-navy font-semibold tabular-nums">
|
||||||
</span>
|
|
||||||
<span className="text-ink-muted tabular-nums">
|
|
||||||
{pct}% · {s.avg_chars.toLocaleString()} תווים
|
{pct}% · {s.avg_chars.toLocaleString()} תווים
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 rounded bg-rule-soft overflow-hidden">
|
<div className="h-2.5 rounded-full bg-rule-soft overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gradient-to-l from-gold to-gold-deep"
|
className="h-full rounded-full"
|
||||||
style={{ width: `${pct}%` }}
|
style={{ width: `${pct}%`, backgroundColor: fill }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -149,50 +149,92 @@ export function StyleReportPanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Signature phrases */}
|
{/* Signature phrases + curator stat — two columns (mockup 12) */}
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<div className="grid gap-6 lg:grid-cols-2 items-start">
|
||||||
<CardContent className="px-6 py-5">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3>
|
<CardContent className="px-6 py-5">
|
||||||
{data.signature_phrases.headline && (
|
<h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3>
|
||||||
<p className="text-[0.78rem] text-gold-deep mb-4">
|
{data.signature_phrases.headline && (
|
||||||
{data.signature_phrases.headline}
|
<p className="text-[0.78rem] text-gold-deep mb-3">
|
||||||
</p>
|
{data.signature_phrases.headline}
|
||||||
)}
|
</p>
|
||||||
{data.signature_phrases.items.length === 0 ? (
|
)}
|
||||||
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
|
{data.signature_phrases.items.length === 0 ? (
|
||||||
) : (
|
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
|
||||||
<ol className="space-y-2">
|
) : (
|
||||||
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
|
<ol>
|
||||||
<li
|
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
|
||||||
key={`${p.type}-${i}`}
|
<li
|
||||||
className="flex items-start gap-3 rounded border border-rule bg-parchment/40 px-3 py-2"
|
key={`${p.type}-${i}`}
|
||||||
>
|
className="flex items-baseline gap-2.5 py-2.5 border-b border-rule-soft last:border-b-0"
|
||||||
<span className="text-[0.7rem] text-ink-muted tabular-nums shrink-0 mt-0.5">
|
|
||||||
#{i + 1}
|
|
||||||
</span>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-ink leading-relaxed text-sm">{p.text}</p>
|
|
||||||
{p.context && (
|
|
||||||
<p className="text-[0.7rem] text-ink-muted mt-0.5">
|
|
||||||
{p.context}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className="
|
|
||||||
shrink-0 text-[0.72rem] rounded-full
|
|
||||||
bg-gold-wash text-gold-deep border border-gold/40
|
|
||||||
px-2 py-0.5 tabular-nums
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
×{p.frequency}
|
<span className="w-5 shrink-0 text-gold-deep font-bold tabular-nums text-sm">
|
||||||
</span>
|
{i + 1}
|
||||||
</li>
|
</span>
|
||||||
))}
|
<div className="flex-1 min-w-0">
|
||||||
</ol>
|
<p className="text-ink-soft leading-relaxed text-[0.85rem] m-0">
|
||||||
)}
|
{p.text}
|
||||||
</CardContent>
|
</p>
|
||||||
</Card>
|
{p.context && (
|
||||||
|
<p className="text-[0.7rem] text-ink-muted mt-0.5 m-0">
|
||||||
|
{p.context}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-[0.72rem] font-semibold rounded-full bg-gold-wash text-gold-deep border border-rule px-2.5 py-0.5 tabular-nums whitespace-nowrap">
|
||||||
|
×{p.frequency}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Curator — surfaced style findings (INV-LRN1 writer gate) */}
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5 flex flex-col gap-4">
|
||||||
|
<h3 className="text-navy text-lg m-0">אוצֵר — ממצאי-סגנון</h3>
|
||||||
|
<div className="flex items-center gap-4 rounded-lg border border-success bg-success-bg px-[18px] py-3.5">
|
||||||
|
<span className="text-success font-bold text-[1.75rem] leading-none tabular-nums">
|
||||||
|
{curator.data ? curator.data.findings_approved : "—"}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<b className="text-navy text-sm font-semibold block">
|
||||||
|
ממצאים מאושרים (זורמים לכותב)
|
||||||
|
</b>
|
||||||
|
<span className="text-ink-muted text-[0.78rem]">
|
||||||
|
אושרו ע״י היו״ר · review_status=approved
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3.5">
|
||||||
|
<div className="flex-1 rounded-lg border border-rule bg-warn-bg px-3.5 py-3">
|
||||||
|
<div className="text-warn font-bold text-[1.35rem] tabular-nums leading-none">
|
||||||
|
{curator.data
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
curator.data.total_findings -
|
||||||
|
curator.data.findings_approved,
|
||||||
|
)
|
||||||
|
: "—"}
|
||||||
|
</div>
|
||||||
|
<div className="text-[0.78rem] text-ink-soft mt-1">לא-מאושרים</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 rounded-lg border border-rule bg-rule-soft px-3.5 py-3">
|
||||||
|
<div className="text-ink-muted font-bold text-[1.35rem] tabular-nums leading-none">
|
||||||
|
{curator.data ? curator.data.total_findings : "—"}
|
||||||
|
</div>
|
||||||
|
<div className="text-[0.78rem] text-ink-soft mt-1">סך ממצאים</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-[0.72rem] text-ink-muted leading-relaxed m-0">
|
||||||
|
רק ממצא מאושר זורם לכותב (INV-LRN1). ממתינים ונדחים אינם משפיעים על
|
||||||
|
הטיוטות.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user