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:
@@ -3800,11 +3800,13 @@ async def list_digests(
|
|||||||
concept_tag: str = "",
|
concept_tag: str = "",
|
||||||
linked: bool | None = None,
|
linked: bool | None = None,
|
||||||
search: str = "",
|
search: str = "",
|
||||||
|
publication: str = "",
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""List digests with simple filters. linked=True/False filters on whether
|
"""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()
|
pool = await get_pool()
|
||||||
conditions: list[str] = []
|
conditions: list[str] = []
|
||||||
params: list = []
|
params: list = []
|
||||||
@@ -3813,6 +3815,10 @@ async def list_digests(
|
|||||||
conditions.append(f"practice_area = ${idx}")
|
conditions.append(f"practice_area = ${idx}")
|
||||||
params.append(practice_area)
|
params.append(practice_area)
|
||||||
idx += 1
|
idx += 1
|
||||||
|
if publication:
|
||||||
|
conditions.append(f"publication = ${idx}")
|
||||||
|
params.append(publication)
|
||||||
|
idx += 1
|
||||||
if concept_tag:
|
if concept_tag:
|
||||||
conditions.append(f"concept_tag ILIKE ${idx}")
|
conditions.append(f"concept_tag ILIKE ${idx}")
|
||||||
params.append(f"%{concept_tag}%")
|
params.append(f"%{concept_tag}%")
|
||||||
|
|||||||
@@ -404,12 +404,13 @@ async def list_digests(
|
|||||||
concept_tag: str = "",
|
concept_tag: str = "",
|
||||||
linked: bool | None = None,
|
linked: bool | None = None,
|
||||||
search: str = "",
|
search: str = "",
|
||||||
|
publication: str = "",
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
return await db.list_digests(
|
return await db.list_digests(
|
||||||
practice_area=practice_area, concept_tag=concept_tag, linked=linked,
|
practice_area=practice_area, concept_tag=concept_tag, linked=linked,
|
||||||
search=search, limit=limit, offset=offset,
|
search=search, publication=publication, limit=limit, offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export function DigestCard({
|
|||||||
יומון {digest.yomon_number}
|
יומון {digest.yomon_number}
|
||||||
</span>
|
</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.digest_date && <span>· {formatDate(digest.digest_date)}</span>}
|
||||||
{digest.practice_area && (
|
{digest.practice_area && (
|
||||||
<span>· {practiceAreaLabel(digest.practice_area)}</span>
|
<span>· {practiceAreaLabel(digest.practice_area)}</span>
|
||||||
@@ -53,6 +58,11 @@ export function DigestCard({
|
|||||||
עדכון
|
עדכון
|
||||||
</Badge>
|
</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" && (
|
{digest.extraction_status !== "completed" && (
|
||||||
<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.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
|
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
|
||||||
|
|||||||
@@ -18,13 +18,21 @@ import { DigestUploadDialog } from "./digest-upload-dialog";
|
|||||||
|
|
||||||
type LinkedFilter = "all" | "linked" | "unlinked";
|
type LinkedFilter = "all" | "linked" | "unlinked";
|
||||||
|
|
||||||
|
const PUBLICATIONS = [
|
||||||
|
{ value: "_all", label: "כל המקורות" },
|
||||||
|
{ value: "כל יום", label: "כל יום (יומי)" },
|
||||||
|
{ value: 'עו"ד על נדל"ן', label: 'עו"ד על נדל"ן (חודשי)' },
|
||||||
|
];
|
||||||
|
|
||||||
export function DigestListPanel() {
|
export function DigestListPanel() {
|
||||||
const [practiceArea, setPracticeArea] = useState<PracticeArea>("");
|
const [practiceArea, setPracticeArea] = useState<PracticeArea>("");
|
||||||
const [linked, setLinked] = useState<LinkedFilter>("all");
|
const [linked, setLinked] = useState<LinkedFilter>("all");
|
||||||
|
const [publication, setPublication] = useState<string>("_all");
|
||||||
|
|
||||||
const filters: DigestListFilters = {
|
const filters: DigestListFilters = {
|
||||||
practiceArea: practiceArea || undefined,
|
practiceArea: practiceArea || undefined,
|
||||||
linked: linked === "all" ? undefined : linked === "linked",
|
linked: linked === "all" ? undefined : linked === "linked",
|
||||||
|
publication: publication === "_all" ? undefined : publication,
|
||||||
limit: 200,
|
limit: 200,
|
||||||
};
|
};
|
||||||
const { data, isLoading, error } = useDigests(filters);
|
const { data, isLoading, error } = useDigests(filters);
|
||||||
@@ -66,6 +74,17 @@ export function DigestListPanel() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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]">
|
<div className="min-w-[170px]">
|
||||||
<label className="text-[0.78rem] text-ink-muted">קישור לפסק</label>
|
<label className="text-[0.78rem] text-ink-muted">קישור לפסק</label>
|
||||||
<Select value={linked} onValueChange={(v) => setLinked(v as LinkedFilter)}>
|
<Select value={linked} onValueChange={(v) => setLinked(v as LinkedFilter)}>
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export type DigestListFilters = {
|
|||||||
/** undefined = all; true = linked to a ruling; false = unlinked (open gap) */
|
/** undefined = all; true = linked to a ruling; false = unlinked (open gap) */
|
||||||
linked?: boolean;
|
linked?: boolean;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
/** source publication: 'כל יום' (daily) | 'עו"ד על נדל"ן' (monthly bulletin) */
|
||||||
|
publication?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
};
|
};
|
||||||
@@ -94,6 +96,7 @@ export function useDigests(filters: DigestListFilters = {}) {
|
|||||||
if (filters.conceptTag) p.set("concept_tag", filters.conceptTag);
|
if (filters.conceptTag) p.set("concept_tag", filters.conceptTag);
|
||||||
if (filters.linked !== undefined) p.set("linked", String(filters.linked));
|
if (filters.linked !== undefined) p.set("linked", String(filters.linked));
|
||||||
if (filters.search) p.set("search", filters.search);
|
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.limit) p.set("limit", String(filters.limit));
|
||||||
if (filters.offset) p.set("offset", String(filters.offset));
|
if (filters.offset) p.set("offset", String(filters.offset));
|
||||||
const qs = p.toString();
|
const qs = p.toString();
|
||||||
|
|||||||
@@ -5975,12 +5975,13 @@ async def digest_list(
|
|||||||
concept_tag: str = "",
|
concept_tag: str = "",
|
||||||
linked: bool | None = None,
|
linked: bool | None = None,
|
||||||
search: str = "",
|
search: str = "",
|
||||||
|
publication: str = "",
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
):
|
):
|
||||||
rows = await digest_service.list_digests(
|
rows = await digest_service.list_digests(
|
||||||
practice_area=practice_area, concept_tag=concept_tag, linked=linked,
|
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)}
|
return {"items": rows, "count": len(rows)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user