feat(ui): תור-אישור הלכות מאוחד — 2 תצוגות לפי פעולה (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s

מבטל את ה-toggle "תור נקי / דורש תיקון-חילוץ" שבו "תור נקי" ריק
לגמרי (כל ההלכות-הנקיות נפתרו), והעבודה האמיתית חבויה מאחורי
הכפתור השני שגם מערבב התלבטות-פאנל עם פגמי-חילוץ. אושר ב-Claude
Design (כרטיס 19-halacha-queue-unified).

במקום זה — תור אחד, fetch אחד, פיצול client-side לפי **סוג-הפעולה**:
- "להכרעתך" = הלכות שהפאנל דן בהן (יש panel_round) או נקיות →
  אשר/דחה, עם טבלת-ההתלבטות; ממוין פיצול-פאנל-תחילה (FU-3).
- "דורש תיקון-חילוץ" = מסומנות-דגל שלא עברו התלבטות → תיקון-חילוץ.

`useHalachotPending` אוחד לקריאה אחת (exclude_low_quality=false +
order_by_priority + cluster + include_equivalents + include_panel_round);
נוסף `isExtractionFixItem(h)` (= !panel_round && יש דגל). PendingPanel
מפצל ב-useMemo, segmented-control עם מוני שני הדליים. אפס שינוי-backend
(הפרמטרים כבר קיימים מ-#220/#222).

display-only, שער-אישור יחיד (INV-IA/G10). ולידציה: tsc + eslint נקי.
חלק מ-#133.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 07:29:39 +00:00
parent a0b3c17381
commit f0a8af30dc
2 changed files with 68 additions and 60 deletions

View File

@@ -10,7 +10,8 @@ import { Textarea } from "@/components/ui/textarea";
import { CorroborationBadge } from "./corroboration-badge";
import { practiceAreaLabel } from "./practice-area";
import {
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot,
isExtractionFixItem, type Halacha,
} from "@/lib/api/precedent-library";
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
@@ -628,23 +629,29 @@ function RestorePanel({
// ─── Pending queue panel (main review flow) ───────────────────────────────────
function PendingPanel() {
// #84.1 — "clean" = quality-gated + prioritized + clustered review queue;
// "needsfix" = the flagged 'needs extraction fix' bucket.
const [view, setView] = useState<"clean" | "needsfix">("clean");
const { data, isPending, error } = useHalachotPending({
limit: 500, needsFix: view === "needsfix",
});
// #133 unified queue — ONE fetch of all pending, split client-side by ACTION:
// "judgment" = items the panel deliberated (or clean) → chair approves/rejects;
// "fix" = flagged-but-never-adjudicated → extraction repair. (No empty "תור נקי".)
const [view, setView] = useState<"judgment" | "fix">("judgment");
const { data, isPending, error } = useHalachotPending({ limit: 500 });
const update = useUpdateHalacha();
const batch = useBatchReviewHalachot();
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [focusedId, setFocusedId] = useState<string | null>(null);
const groups = useMemo<Group[]>(
() => buildGroups(data?.items ?? []),
[data],
);
const allItems = useMemo(() => data?.items ?? [], [data]);
const judgmentCount = useMemo(
() => allItems.filter((h) => !isExtractionFixItem(h)).length, [allItems]);
const fixCount = useMemo(
() => allItems.filter(isExtractionFixItem).length, [allItems]);
const totalCount = data?.items.length ?? 0;
const segmentItems = useMemo(
() => allItems.filter((h) =>
view === "fix" ? isExtractionFixItem(h) : !isExtractionFixItem(h)),
[allItems, view],
);
const groups = useMemo<Group[]>(() => buildGroups(segmentItems), [segmentItems]);
const totalCount = segmentItems.length;
const visibleItems = useMemo<ReviewItem[]>(() => {
const out: ReviewItem[] = [];
@@ -766,24 +773,26 @@ function PendingPanel() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focused, visibleItems]);
const segBtn = (key: "judgment" | "fix", label: string, count: number) => (
<Button
size="sm"
variant={view === key ? "default" : "ghost"}
className={view === key ? "bg-gold text-navy hover:bg-gold-deep" : ""}
onClick={() => setView(key)}
>
{label}
<span className={`ms-2 text-[0.7rem] px-1.5 py-0.5 rounded-full tabular-nums
${view === key ? "bg-navy/15" : "bg-black/10 text-ink-muted"}`}>
{count}
</span>
</Button>
);
// #133 — two segments by ACTION (deliberated→judge vs flagged→fix); no empty "תור נקי".
const viewToggle = (
<div className="flex items-center gap-1 rounded-lg border border-rule p-0.5 bg-rule-soft/30 w-fit">
<Button
size="sm"
variant={view === "clean" ? "default" : "ghost"}
className={view === "clean" ? "bg-gold text-navy hover:bg-gold-deep" : ""}
onClick={() => setView("clean")}
>
תור נקי
</Button>
<Button
size="sm"
variant={view === "needsfix" ? "default" : "ghost"}
className={view === "needsfix" ? "bg-gold text-navy hover:bg-gold-deep" : ""}
onClick={() => setView("needsfix")}
>
דורש תיקון-חילוץ
</Button>
{segBtn("judgment", "להכרעתך", judgmentCount)}
{segBtn("fix", "דורש תיקון-חילוץ", fixCount)}
</div>
);
@@ -804,14 +813,14 @@ function PendingPanel() {
body = (
<div className="text-center text-ink-muted py-16">
<p className="text-lg">
{view === "needsfix"
? "בקט התיקון ריק — אין הלכות מסומנות-איכות."
: "אין הלכות נקיות הממתינות לאישור."}
{view === "fix"
? "אין הלכות הממתינות לתיקון-חילוץ."
: "אין הלכות הממתינות להכרעתך."}
</p>
<p className="text-sm mt-2">
{view === "needsfix"
? "פריטים שסומנו (ציטוט לא-מאומת, יישום, כפילות-קרובה וכו') יופיעו כאן."
: עלה פסיקה חדשה — ההלכות שיחולצו ממנה יופיעו כאן."}
{view === "fix"
? "פריטים שסומנו בפגם-חילוץ (ציטוט לא-מאומת / קטוע / כפילות) ושלא עברו התלבטות-פאנל יופיעו כאן."
: לכות שהפאנל הכריע או דן בהן יופיעו כאן לאישורך."}
</p>
</div>
);
@@ -821,8 +830,11 @@ function PendingPanel() {
<div className="flex items-center gap-3 text-sm text-ink-muted flex-wrap">
<span>
<span className="text-navy font-semibold">{totalCount}</span>
{view === "needsfix" ? " מסומנות" : " ממתינות"}
{view === "fix" ? " לתיקון-חילוץ" : " להכרעה"}
{" "}ב-<span className="text-navy font-semibold">{groups.length}</span> פסיקות
{view === "judgment" && (
<span className="text-[0.72rem] ms-2">· ממוין: פיצול-פאנל תחילה</span>
)}
</span>
<span className="me-auto text-[0.72rem]">
ניווט: <kbd className="bg-rule-soft px-1.5 rounded">J</kbd>/<kbd className="bg-rule-soft px-1.5 rounded">K</kbd>

View File

@@ -603,37 +603,33 @@ export function useRequestHalachotExtraction() {
/** #84.1/#84.2/#84.3 — the chair review queue.
*
* Default ("clean") view: quality-gated (flagged items hidden), priority-ordered
* (most-uncertain/negatively-treated first), and near-duplicate-clustered into
* one card. Pass `needsFix: true` for the 'needs extraction fix' bucket — every
* pending item carrying a quality flag (filtered client-side). */
export function useHalachotPending(
opts: { limit?: number; needsFix?: boolean } = {},
) {
const { limit = 200, needsFix = false } = opts;
const qs = needsFix
? `review_status=pending_review&exclude_low_quality=false`
+ `&include_panel_round=true&limit=${limit}`
: `review_status=pending_review&exclude_low_quality=true`
+ `&order_by_priority=true&cluster=true&include_equivalents=true`
+ `&include_panel_round=true&limit=${limit}`;
* ONE fetch of the whole pending set (#133 unified queue): all pending halachot,
* priority-ordered (panel-split first → most-uncertain → oldest, #84.3/FU-3),
* near-duplicate-clustered, with the latest panel deliberation attached. The
* review panel splits this client-side by ACTION — "להכרעה" (has a panel round)
* vs "תיקון-חילוץ" (flagged, never adjudicated) — instead of the old empty
* clean/needsfix toggle. */
export function useHalachotPending(opts: { limit?: number } = {}) {
const { limit = 200 } = opts;
const qs = `review_status=pending_review&exclude_low_quality=false`
+ `&order_by_priority=true&cluster=true&include_equivalents=true`
+ `&include_panel_round=true&limit=${limit}`;
return useQuery({
queryKey: [...libraryKeys.halachotPending(), needsFix ? "needsfix" : "clean"],
queryFn: async ({ signal }) => {
const res = await apiRequest<{ items: Halacha[]; count: number }>(
`/api/halachot?${qs}`,
{ signal },
);
if (!needsFix) return res;
// needs-fix bucket = pending items that carry a quality flag
const items = res.items.filter((h) => (h.quality_flags?.length ?? 0) > 0);
return { items, count: items.length };
},
queryKey: libraryKeys.halachotPending(),
queryFn: ({ signal }) =>
apiRequest<{ items: Halacha[]; count: number }>(`/api/halachot?${qs}`, { signal }),
staleTime: 5_000,
refetchOnMount: "always",
});
}
/** A pending item belongs in the "needs extraction fix" segment when it carries a
* quality flag AND the panel never deliberated it (no round). Everything else —
* deliberated items and clean items — is a chair-judgment item. (#133 unified queue) */
export function isExtractionFixItem(h: Halacha): boolean {
return !h.panel_round && (h.quality_flags?.length ?? 0) > 0;
}
export function useHalachotByStatus(status: string, limit = 300) {
return useQuery({
queryKey: libraryKeys.halachot({ review_status: status, limit: String(limit) }),