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>
This commit is contained in:
2026-06-08 08:14:23 +00:00
parent 05e8373d22
commit 5745d36bb4
6 changed files with 43 additions and 3 deletions

View File

@@ -3800,11 +3800,13 @@ async def list_digests(
concept_tag: str = "",
linked: bool | None = None,
search: str = "",
publication: str = "",
limit: int = 100,
offset: int = 0,
) -> list[dict]:
"""List digests with simple filters. linked=True/False filters on whether
the underlying ruling is in the library yet (INV-DIG3 gap surfacing)."""
the underlying ruling is in the library yet (INV-DIG3 gap surfacing).
publication filters the source ('כל יום' daily vs 'עו"ד על נדל"ן' monthly)."""
pool = await get_pool()
conditions: list[str] = []
params: list = []
@@ -3813,6 +3815,10 @@ async def list_digests(
conditions.append(f"practice_area = ${idx}")
params.append(practice_area)
idx += 1
if publication:
conditions.append(f"publication = ${idx}")
params.append(publication)
idx += 1
if concept_tag:
conditions.append(f"concept_tag ILIKE ${idx}")
params.append(f"%{concept_tag}%")

View File

@@ -404,12 +404,13 @@ async def list_digests(
concept_tag: str = "",
linked: bool | None = None,
search: str = "",
publication: str = "",
limit: int = 100,
offset: int = 0,
) -> list[dict]:
return await db.list_digests(
practice_area=practice_area, concept_tag=concept_tag, linked=linked,
search=search, limit=limit, offset=offset,
search=search, publication=publication, limit=limit, offset=offset,
)

View File

@@ -44,6 +44,11 @@ export function DigestCard({
יומון {digest.yomon_number}
</span>
)}
{digest.publication && digest.publication !== "כל יום" && (
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
{digest.publication}
</Badge>
)}
{digest.digest_date && <span>· {formatDate(digest.digest_date)}</span>}
{digest.practice_area && (
<span>· {practiceAreaLabel(digest.practice_area)}</span>
@@ -53,6 +58,11 @@ export function DigestCard({
עדכון
</Badge>
)}
{digest.digest_kind === "article" && (
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-300 text-[0.65rem]">
מאמר
</Badge>
)}
{digest.extraction_status !== "completed" && (
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}

View File

@@ -18,13 +18,21 @@ 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);
@@ -66,6 +74,17 @@ export function DigestListPanel() {
</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)}>

View File

@@ -66,6 +66,8 @@ export type DigestListFilters = {
/** undefined = all; true = linked to a ruling; false = unlinked (open gap) */
linked?: boolean;
search?: string;
/** source publication: 'כל יום' (daily) | 'עו"ד על נדל"ן' (monthly bulletin) */
publication?: string;
limit?: number;
offset?: number;
};
@@ -94,6 +96,7 @@ export function useDigests(filters: DigestListFilters = {}) {
if (filters.conceptTag) p.set("concept_tag", filters.conceptTag);
if (filters.linked !== undefined) p.set("linked", String(filters.linked));
if (filters.search) p.set("search", filters.search);
if (filters.publication) p.set("publication", filters.publication);
if (filters.limit) p.set("limit", String(filters.limit));
if (filters.offset) p.set("offset", String(filters.offset));
const qs = p.toString();

View File

@@ -5975,12 +5975,13 @@ async def digest_list(
concept_tag: str = "",
linked: bool | None = None,
search: str = "",
publication: str = "",
limit: int = 100,
offset: int = 0,
):
rows = await digest_service.list_digests(
practice_area=practice_area, concept_tag=concept_tag, linked=linked,
search=search, limit=limit, offset=offset,
search=search, publication=publication, limit=limit, offset=offset,
)
return {"items": rows, "count": len(rows)}