feat(ui): תור-אישור הלכות מאוחד — 2 תצוגות לפי פעולה (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
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:
@@ -10,7 +10,8 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { CorroborationBadge } from "./corroboration-badge";
|
import { CorroborationBadge } from "./corroboration-badge";
|
||||||
import { practiceAreaLabel } from "./practice-area";
|
import { practiceAreaLabel } from "./practice-area";
|
||||||
import {
|
import {
|
||||||
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
|
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot,
|
||||||
|
isExtractionFixItem, type Halacha,
|
||||||
} from "@/lib/api/precedent-library";
|
} from "@/lib/api/precedent-library";
|
||||||
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
|
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
|
||||||
|
|
||||||
@@ -628,23 +629,29 @@ function RestorePanel({
|
|||||||
// ─── Pending queue panel (main review flow) ───────────────────────────────────
|
// ─── Pending queue panel (main review flow) ───────────────────────────────────
|
||||||
|
|
||||||
function PendingPanel() {
|
function PendingPanel() {
|
||||||
// #84.1 — "clean" = quality-gated + prioritized + clustered review queue;
|
// #133 unified queue — ONE fetch of all pending, split client-side by ACTION:
|
||||||
// "needsfix" = the flagged 'needs extraction fix' bucket.
|
// "judgment" = items the panel deliberated (or clean) → chair approves/rejects;
|
||||||
const [view, setView] = useState<"clean" | "needsfix">("clean");
|
// "fix" = flagged-but-never-adjudicated → extraction repair. (No empty "תור נקי".)
|
||||||
const { data, isPending, error } = useHalachotPending({
|
const [view, setView] = useState<"judgment" | "fix">("judgment");
|
||||||
limit: 500, needsFix: view === "needsfix",
|
const { data, isPending, error } = useHalachotPending({ limit: 500 });
|
||||||
});
|
|
||||||
const update = useUpdateHalacha();
|
const update = useUpdateHalacha();
|
||||||
const batch = useBatchReviewHalachot();
|
const batch = useBatchReviewHalachot();
|
||||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||||
const [focusedId, setFocusedId] = useState<string | null>(null);
|
const [focusedId, setFocusedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const groups = useMemo<Group[]>(
|
const allItems = useMemo(() => data?.items ?? [], [data]);
|
||||||
() => buildGroups(data?.items ?? []),
|
const judgmentCount = useMemo(
|
||||||
[data],
|
() => 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 visibleItems = useMemo<ReviewItem[]>(() => {
|
||||||
const out: ReviewItem[] = [];
|
const out: ReviewItem[] = [];
|
||||||
@@ -766,24 +773,26 @@ function PendingPanel() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [focused, visibleItems]);
|
}, [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 = (
|
const viewToggle = (
|
||||||
<div className="flex items-center gap-1 rounded-lg border border-rule p-0.5 bg-rule-soft/30 w-fit">
|
<div className="flex items-center gap-1 rounded-lg border border-rule p-0.5 bg-rule-soft/30 w-fit">
|
||||||
<Button
|
{segBtn("judgment", "להכרעתך", judgmentCount)}
|
||||||
size="sm"
|
{segBtn("fix", "דורש תיקון-חילוץ", fixCount)}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -804,14 +813,14 @@ function PendingPanel() {
|
|||||||
body = (
|
body = (
|
||||||
<div className="text-center text-ink-muted py-16">
|
<div className="text-center text-ink-muted py-16">
|
||||||
<p className="text-lg">
|
<p className="text-lg">
|
||||||
{view === "needsfix"
|
{view === "fix"
|
||||||
? "בקט התיקון ריק — אין הלכות מסומנות-איכות."
|
? "אין הלכות הממתינות לתיקון-חילוץ."
|
||||||
: "אין הלכות נקיות הממתינות לאישור."}
|
: "אין הלכות הממתינות להכרעתך."}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm mt-2">
|
<p className="text-sm mt-2">
|
||||||
{view === "needsfix"
|
{view === "fix"
|
||||||
? "פריטים שסומנו (ציטוט לא-מאומת, יישום, כפילות-קרובה וכו') יופיעו כאן."
|
? "פריטים שסומנו בפגם-חילוץ (ציטוט לא-מאומת / קטוע / כפילות) ושלא עברו התלבטות-פאנל יופיעו כאן."
|
||||||
: "העלה פסיקה חדשה — ההלכות שיחולצו ממנה יופיעו כאן."}
|
: "הלכות שהפאנל הכריע או דן בהן יופיעו כאן לאישורך."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -821,8 +830,11 @@ function PendingPanel() {
|
|||||||
<div className="flex items-center gap-3 text-sm text-ink-muted flex-wrap">
|
<div className="flex items-center gap-3 text-sm text-ink-muted flex-wrap">
|
||||||
<span>
|
<span>
|
||||||
<span className="text-navy font-semibold">{totalCount}</span>
|
<span className="text-navy font-semibold">{totalCount}</span>
|
||||||
{view === "needsfix" ? " מסומנות" : " ממתינות"}
|
{view === "fix" ? " לתיקון-חילוץ" : " להכרעה"}
|
||||||
{" "}ב-<span className="text-navy font-semibold">{groups.length}</span> פסיקות
|
{" "}ב-<span className="text-navy font-semibold">{groups.length}</span> פסיקות
|
||||||
|
{view === "judgment" && (
|
||||||
|
<span className="text-[0.72rem] ms-2">· ממוין: פיצול-פאנל תחילה</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="me-auto text-[0.72rem]">
|
<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>
|
ניווט: <kbd className="bg-rule-soft px-1.5 rounded">J</kbd>/<kbd className="bg-rule-soft px-1.5 rounded">K</kbd>
|
||||||
|
|||||||
@@ -603,37 +603,33 @@ export function useRequestHalachotExtraction() {
|
|||||||
|
|
||||||
/** #84.1/#84.2/#84.3 — the chair review queue.
|
/** #84.1/#84.2/#84.3 — the chair review queue.
|
||||||
*
|
*
|
||||||
* Default ("clean") view: quality-gated (flagged items hidden), priority-ordered
|
* ONE fetch of the whole pending set (#133 unified queue): all pending halachot,
|
||||||
* (most-uncertain/negatively-treated first), and near-duplicate-clustered into
|
* priority-ordered (panel-split first → most-uncertain → oldest, #84.3/FU-3),
|
||||||
* one card. Pass `needsFix: true` for the 'needs extraction fix' bucket — every
|
* near-duplicate-clustered, with the latest panel deliberation attached. The
|
||||||
* pending item carrying a quality flag (filtered client-side). */
|
* review panel splits this client-side by ACTION — "להכרעה" (has a panel round)
|
||||||
export function useHalachotPending(
|
* vs "תיקון-חילוץ" (flagged, never adjudicated) — instead of the old empty
|
||||||
opts: { limit?: number; needsFix?: boolean } = {},
|
* clean/needsfix toggle. */
|
||||||
) {
|
export function useHalachotPending(opts: { limit?: number } = {}) {
|
||||||
const { limit = 200, needsFix = false } = opts;
|
const { limit = 200 } = opts;
|
||||||
const qs = needsFix
|
const qs = `review_status=pending_review&exclude_low_quality=false`
|
||||||
? `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`
|
+ `&order_by_priority=true&cluster=true&include_equivalents=true`
|
||||||
+ `&include_panel_round=true&limit=${limit}`;
|
+ `&include_panel_round=true&limit=${limit}`;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [...libraryKeys.halachotPending(), needsFix ? "needsfix" : "clean"],
|
queryKey: libraryKeys.halachotPending(),
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: ({ signal }) =>
|
||||||
const res = await apiRequest<{ items: Halacha[]; count: number }>(
|
apiRequest<{ items: Halacha[]; count: number }>(`/api/halachot?${qs}`, { signal }),
|
||||||
`/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 };
|
|
||||||
},
|
|
||||||
staleTime: 5_000,
|
staleTime: 5_000,
|
||||||
refetchOnMount: "always",
|
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) {
|
export function useHalachotByStatus(status: string, limit = 300) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: libraryKeys.halachot({ review_status: status, limit: String(limit) }),
|
queryKey: libraryKeys.halachot({ review_status: status, limit: String(limit) }),
|
||||||
|
|||||||
Reference in New Issue
Block a user