diff --git a/web-ui/src/app/precedents/[id]/page.tsx b/web-ui/src/app/precedents/[id]/page.tsx new file mode 100644 index 0000000..71e0bbf --- /dev/null +++ b/web-ui/src/app/precedents/[id]/page.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { use, useState } from "react"; +import Link from "next/link"; +import { Pencil } from "lucide-react"; +import { AppShell } from "@/components/app-shell"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { usePrecedent } from "@/lib/api/precedent-library"; +import { PrecedentEditSheet } from "@/components/precedents/precedent-edit-sheet"; +import { ExtractedHalachotSection } from "@/components/precedents/extracted-halachot"; + +const PRACTICE_AREA_LABELS: Record = { + rishuy_uvniya: "רישוי ובנייה", + betterment_levy: "היטל השבחה", + compensation_197: "פיצויים (197)", +}; + +const SOURCE_TYPE_LABELS: Record = { + court_ruling: "פסק דין", + appeals_committee: "ועדת ערר", +}; + +/* Next 16 breaking change: route params are now a Promise. + * The `use()` hook unwraps them inside a client component. */ +export default function PrecedentDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = use(params); + const [editing, setEditing] = useState(false); + const { data, isPending, error } = usePrecedent(id); + + return ( + +
+
+ +
+ + {error ? ( + + +

שגיאה בטעינת הפסיקה

+

{error.message}

+ +
+
+ ) : isPending || !data ? ( +
+ {[...Array(5)].map((_, i) => )} +
+ ) : ( + <> + + +
+
+

+ {data.case_name || "—"} +

+
+ {data.case_number} +
+
+ +
+ +
+ {data.practice_area ? ( + + {PRACTICE_AREA_LABELS[data.practice_area] ?? data.practice_area} + + ) : null} + {data.source_type ? ( + + {SOURCE_TYPE_LABELS[data.source_type] ?? data.source_type} + + ) : null} + {data.precedent_level ? ( + + {data.precedent_level} + + ) : null} + {data.is_binding ? ( + + הלכה מחייבת + + ) : null} + {data.court ? ( + {data.court} + ) : null} + {data.date ? ( + + {data.date.slice(0, 10)} + + ) : null} +
+ + {data.headnote ? ( +
+

Headnote

+

+ {data.headnote} +

+
+ ) : null} + + {data.summary ? ( +
+

תקציר

+

+ {data.summary} +

+
+ ) : null} + + {(data as { key_quote?: string }).key_quote ? ( +
+

ציטוט מרכזי

+
+ {(data as { key_quote?: string }).key_quote} +
+
+ ) : null} + + {data.subject_tags?.length ? ( +
+ {data.subject_tags.map((t) => ( + + {t} + + ))} +
+ ) : null} +
+
+ + + + + + + + )} + + setEditing(open)} + /> +
+
+ ); +} diff --git a/web-ui/src/components/precedents/extracted-halachot.tsx b/web-ui/src/components/precedents/extracted-halachot.tsx new file mode 100644 index 0000000..3d1040e --- /dev/null +++ b/web-ui/src/components/precedents/extracted-halachot.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import type { Halacha } from "@/lib/api/precedent-library"; + +const RULE_TYPE_LABELS: Record = { + binding: "הלכה מחייבת", + interpretive: "פרשני", + procedural: "פרוצדורלי", + obiter: "אמרת אגב", + application: "יישום הלכה", + persuasive: "משכנע", +}; + +type StatusFilter = "all" | "approved" | "pending" | "rejected"; + +export function ReviewStatusPill({ status }: { status: Halacha["review_status"] }) { + if (status === "approved" || status === "published") { + return ( + + מאושרת + + ); + } + if (status === "pending_review") { + return ( + + ממתינה + + ); + } + return ( + + נדחתה + + ); +} + +/* Read-only roll-up of every halacha extracted from a precedent — + * approved + pending + rejected. The "ממתין לאישור" tab only surfaces + * pending items globally; this section is the per-case view. To act on + * an item (approve / edit / reject), go to the review tab — keeping the + * surfaces separated avoids duplicate review UX in two places. */ +export function ExtractedHalachotSection({ halachot }: { halachot: Halacha[] }) { + const [filter, setFilter] = useState("all"); + + const counts = useMemo(() => { + const c = { all: halachot.length, approved: 0, pending: 0, rejected: 0 }; + for (const h of halachot) { + if (h.review_status === "approved" || h.review_status === "published") { + c.approved++; + } else if (h.review_status === "pending_review") { + c.pending++; + } else if (h.review_status === "rejected") { + c.rejected++; + } + } + return c; + }, [halachot]); + + const sorted = useMemo(() => { + const matches = (h: Halacha) => { + if (filter === "all") return true; + if (filter === "approved") { + return h.review_status === "approved" || h.review_status === "published"; + } + if (filter === "pending") return h.review_status === "pending_review"; + return h.review_status === "rejected"; + }; + return halachot + .filter(matches) + .sort((a, b) => a.halacha_index - b.halacha_index); + }, [halachot, filter]); + + if (!halachot.length) { + return ( +
+ עדיין לא חולצו הלכות מהפסיקה הזו. +
+ ); + } + + const tabs: { key: StatusFilter; label: string; count: number }[] = [ + { key: "all", label: "הכל", count: counts.all }, + { key: "approved", label: "מאושרות", count: counts.approved }, + { key: "pending", label: "ממתינות", count: counts.pending }, + { key: "rejected", label: "נדחו", count: counts.rejected }, + ]; + + return ( +
+
+

+ הלכות שחולצו ({counts.all}) +

+
+ {tabs.map((t) => { + const active = filter === t.key; + return ( + + ); + })} +
+
+ + {!sorted.length ? ( +
+ אין הלכות בקטגוריה זו. +
+ ) : ( +
    + {sorted.map((h) => ( +
  1. +
    + + #{h.halacha_index} + + + + {RULE_TYPE_LABELS[h.rule_type] ?? h.rule_type} + + + ביטחון {h.confidence.toFixed(2)} + + {h.page_reference ? ( + + {h.page_reference} + + ) : null} +
    + +

    + {h.rule_statement} +

    + + {h.reasoning_summary ? ( +

    + היגיון: + {h.reasoning_summary} +

    + ) : null} + + {h.supporting_quote ? ( +
    + “{h.supporting_quote}” +
    + ) : null} + + {h.subject_tags?.length ? ( +
    + {h.subject_tags.map((t) => ( + + {t} + + ))} +
    + ) : null} +
  2. + ))} +
+ )} +
+ ); +} diff --git a/web-ui/src/components/precedents/precedent-edit-sheet.tsx b/web-ui/src/components/precedents/precedent-edit-sheet.tsx index 8b9c29e..b39f621 100644 --- a/web-ui/src/components/precedents/precedent-edit-sheet.tsx +++ b/web-ui/src/components/precedents/precedent-edit-sheet.tsx @@ -1,12 +1,11 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Save, Sparkles } from "lucide-react"; import { toast } from "sonner"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -19,207 +18,13 @@ import { usePrecedent, useUpdatePrecedent, useRequestMetadataExtraction, - type Halacha, type PracticeArea, type SourceType, } from "@/lib/api/precedent-library"; import { PRACTICE_AREAS, PRECEDENT_LEVELS, SOURCE_TYPES, } from "./practice-area"; - -const RULE_TYPE_LABELS: Record = { - binding: "הלכה מחייבת", - interpretive: "פרשני", - procedural: "פרוצדורלי", - obiter: "אמרת אגב", - application: "יישום הלכה", - persuasive: "משכנע", -}; - -type StatusFilter = "all" | "approved" | "pending" | "rejected"; - -function ReviewStatusPill({ status }: { status: Halacha["review_status"] }) { - if (status === "approved" || status === "published") { - return ( - - מאושרת - - ); - } - if (status === "pending_review") { - return ( - - ממתינה - - ); - } - return ( - - נדחתה - - ); -} - -/* Read-only roll-up of every halacha extracted from this precedent — - * approved + pending + rejected. The "ממתין לאישור" tab only surfaces - * pending items globally; this section is the per-case view. To act on - * an item (approve / edit / reject), go to the review tab — keeping the - * surfaces separated avoids duplicate review UX in two places. */ -function ExtractedHalachotSection({ halachot }: { halachot: Halacha[] }) { - const [filter, setFilter] = useState("all"); - - const counts = useMemo(() => { - const c = { all: halachot.length, approved: 0, pending: 0, rejected: 0 }; - for (const h of halachot) { - if (h.review_status === "approved" || h.review_status === "published") { - c.approved++; - } else if (h.review_status === "pending_review") { - c.pending++; - } else if (h.review_status === "rejected") { - c.rejected++; - } - } - return c; - }, [halachot]); - - const sorted = useMemo(() => { - const matches = (h: Halacha) => { - if (filter === "all") return true; - if (filter === "approved") { - return h.review_status === "approved" || h.review_status === "published"; - } - if (filter === "pending") return h.review_status === "pending_review"; - return h.review_status === "rejected"; - }; - return halachot - .filter(matches) - .sort((a, b) => a.halacha_index - b.halacha_index); - }, [halachot, filter]); - - if (!halachot.length) { - return ( -
- עדיין לא חולצו הלכות מהפסיקה הזו. -
- ); - } - - const tabs: { key: StatusFilter; label: string; count: number }[] = [ - { key: "all", label: "הכל", count: counts.all }, - { key: "approved", label: "מאושרות", count: counts.approved }, - { key: "pending", label: "ממתינות", count: counts.pending }, - { key: "rejected", label: "נדחו", count: counts.rejected }, - ]; - - return ( -
-
-

- הלכות שחולצו ({counts.all}) -

-
- {tabs.map((t) => { - const active = filter === t.key; - return ( - - ); - })} -
-
- - {!sorted.length ? ( -
- אין הלכות בקטגוריה זו. -
- ) : ( -
    - {sorted.map((h) => ( -
  1. -
    - - #{h.halacha_index} - - - - {RULE_TYPE_LABELS[h.rule_type] ?? h.rule_type} - - - ביטחון {h.confidence.toFixed(2)} - - {h.page_reference ? ( - - {h.page_reference} - - ) : null} -
    - -

    - {h.rule_statement} -

    - - {h.reasoning_summary ? ( -

    - היגיון: - {h.reasoning_summary} -

    - ) : null} - - {h.supporting_quote ? ( -
    - “{h.supporting_quote}” -
    - ) : null} - - {h.subject_tags?.length ? ( -
    - {h.subject_tags.map((t) => ( - - {t} - - ))} -
    - ) : null} -
  2. - ))} -
- )} -
- ); -} +import { ExtractedHalachotSection } from "./extracted-halachot"; type Props = { caseLawId: string | null;