Files
legal-ai/web-ui/src/components/digests/digest-list-panel.tsx
Chaim 5745d36bb4 feat(digests-ui): publication filter + 'מאמר'/source badges for bulletins
משלים את #154 בצד-לקוח:
- פילטר "מקור" בדף /digests (כל המקורות / כל יום / עו"ד על נדל"ן) — backend:
  list_digests + /api/digests מקבלים publication.
- DigestCard: תג "מאמר" ל-digest_kind='article', ו-chip מקור לפרסום שאינו 'כל יום'.

build (webpack) עובר, lint נקי. digests = hand-written types (אין api:types).

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

156 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { Link2, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import {
useDigests, useDeleteDigest, useRelinkDigest, type DigestListFilters,
} from "@/lib/api/digests";
import type { PracticeArea } from "@/lib/api/precedent-library";
import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
import { DigestCard } from "./digest-card";
import { DigestUploadDialog } from "./digest-upload-dialog";
type LinkedFilter = "all" | "linked" | "unlinked";
const PUBLICATIONS = [
{ value: "_all", label: "כל המקורות" },
{ value: "כל יום", label: "כל יום (יומי)" },
{ value: 'עו"ד על נדל"ן', label: 'עו"ד על נדל"ן (חודשי)' },
];
export function DigestListPanel() {
const [practiceArea, setPracticeArea] = useState<PracticeArea>("");
const [linked, setLinked] = useState<LinkedFilter>("all");
const [publication, setPublication] = useState<string>("_all");
const filters: DigestListFilters = {
practiceArea: practiceArea || undefined,
linked: linked === "all" ? undefined : linked === "linked",
publication: publication === "_all" ? undefined : publication,
limit: 200,
};
const { data, isLoading, error } = useDigests(filters);
const del = useDeleteDigest();
const relink = useRelinkDigest();
const onRelink = (id: string) =>
relink.mutate(id, {
onSuccess: (r) =>
r.linked
? toast.success("קושר לפסק המקורי")
: toast.info("הפסק המקורי עדיין לא בקורפוס — העלה אותו לספריית הפסיקה"),
onError: (e) => toast.error(e.message),
});
const onDelete = (id: string) => {
if (!confirm("למחוק את היומון מקורפוס-הגילוי?")) return;
del.mutate(id, {
onSuccess: () => toast.success("נמחק"),
onError: (e) => toast.error(e.message),
});
};
return (
<div className="space-y-4">
<div className="flex items-end gap-3 flex-wrap">
<div className="min-w-[180px]">
<label className="text-[0.78rem] text-ink-muted">תחום</label>
<Select
value={practiceArea || "_all"}
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : (v as PracticeArea))}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="_all">הכל</SelectItem>
{PRACTICE_AREAS.map((a) => (
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="min-w-[180px]">
<label className="text-[0.78rem] text-ink-muted">מקור</label>
<Select value={publication} onValueChange={setPublication}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{PUBLICATIONS.map((p) => (
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="min-w-[170px]">
<label className="text-[0.78rem] text-ink-muted">קישור לפסק</label>
<Select value={linked} onValueChange={(v) => setLinked(v as LinkedFilter)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">הכל</SelectItem>
<SelectItem value="linked">מקושרים</SelectItem>
<SelectItem value="unlinked">לא מקושרים (פער)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="ms-auto">
<DigestUploadDialog />
</div>
</div>
{error ? (
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-5 text-danger text-center">
{error.message}
</div>
) : isLoading ? (
<div className="space-y-3">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-32 w-full" />)}
</div>
) : !data?.items.length ? (
<div className="text-center text-ink-muted py-12">
אין יומונים עדיין. העלה יומון &quot;כל יום&quot;, או הרץ
<span className="font-mono mx-1" dir="ltr">scripts/ingest_digests_batch.py</span>.
</div>
) : (
<div className="space-y-3">
<p className="text-[0.78rem] text-ink-muted">{data.count} יומונים</p>
{data.items.map((d) => (
<DigestCard
key={d.id}
digest={d}
actions={
<>
{!d.linked_case_law_id && (
<Button
variant="outline"
size="sm"
onClick={() => onRelink(d.id)}
disabled={relink.isPending}
>
<Link2 className="w-3.5 h-3.5 me-1" />
נסה לקשר
</Button>
)}
<Button
variant="outline"
size="sm"
className="text-danger hover:bg-danger-bg"
onClick={() => onDelete(d.id)}
disabled={del.isPending}
>
<Trash2 className="w-3.5 h-3.5 me-1" />
מחק
</Button>
</>
}
/>
))}
</div>
)}
</div>
);
}