משלים את #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>
156 lines
5.7 KiB
TypeScript
156 lines
5.7 KiB
TypeScript
"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">
|
||
אין יומונים עדיין. העלה יומון "כל יום", או הרץ
|
||
<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>
|
||
);
|
||
}
|