feat(halacha): טאבים נדחו/אושרו + שחזור הלכה + הסרת placeholders עם שמות
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
- מוסיף טאב "נדחו" לדף האישורים: הלכות שנדחו מופיעות עם כפתורי "אשר" (ישירות) ו-"שחזר לתור"
- מוסיף טאב "אושרו": הלכות שאושרו עם "בטל אישור" ו-"דחה"
- ספירה צבועה על כל טאב (זהב/אדום/כחול)
- מוסיף useHalachotByStatus hook ב-API
- מסיר placeholders עם שמות ("דפנה תמיר") משדות יו"ר
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -324,7 +324,7 @@ export function MissingPrecedentDetailDrawer({ id, onOpenChange }: Props) {
|
||||
id="chair_name"
|
||||
value={chairName}
|
||||
onChange={(e) => setChairName(e.target.value)}
|
||||
placeholder="דפנה תמיר"
|
||||
placeholder=""
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Check, X, Edit2, ChevronDown, ChevronLeft, AlertTriangle, Clock } from "lucide-react";
|
||||
import { Check, X, Edit2, ChevronDown, ChevronLeft, AlertTriangle, Clock, RotateCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -10,7 +10,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { CorroborationBadge } from "./corroboration-badge";
|
||||
import { practiceAreaLabel } from "./practice-area";
|
||||
import {
|
||||
useHalachotPending, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
|
||||
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
|
||||
} from "@/lib/api/precedent-library";
|
||||
|
||||
/** #81 strict-rubric flags — why an item was held back from auto-approval. */
|
||||
@@ -19,21 +19,9 @@ const QUALITY_FLAG_LABELS: Record<string, string> = {
|
||||
truncated_quote: "ציטוט קטוע",
|
||||
thin_restatement: "ניסוח דק",
|
||||
quote_unverified: "ציטוט לא מאומת",
|
||||
nli_unsupported: "כלל לא נגזר מהציטוט",
|
||||
};
|
||||
|
||||
/**
|
||||
* Halacha review queue — the chair-only path between automatic
|
||||
* extraction and agent visibility. Per the project's review policy,
|
||||
* NO halacha is auto-published; every row sits in pending_review until
|
||||
* approved.
|
||||
*
|
||||
* UX: items are grouped by precedent (case_law_id). Groups start
|
||||
* collapsed so the chair picks one ruling at a time. Within an open
|
||||
* group, J/K navigates, A approves, R rejects, E edits. Items inside
|
||||
* each group are sorted by confidence ascending so the doubtful ones
|
||||
* surface first.
|
||||
*/
|
||||
|
||||
function formatDate(iso: string | null | undefined) {
|
||||
if (!iso) return "—";
|
||||
try {
|
||||
@@ -43,9 +31,7 @@ function formatDate(iso: string | null | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
/* The upload form (and Nevo PDFs) embed Unicode bidi marks (RTL/LTR/embedding/
|
||||
* isolate) inside the citation. They render as zero-width but visually push
|
||||
* the text away from where it should sit. Strip for display only. */
|
||||
/* Strip Unicode bidi marks that render as zero-width but shift visual position. */
|
||||
function cleanCitation(s: string | null | undefined): string {
|
||||
if (!s) return "—";
|
||||
return s.replace(/[--]/g, "").trim();
|
||||
@@ -66,6 +52,8 @@ function ruleTypeLabel(t: string): string {
|
||||
|
||||
type EditState = { rule_statement: string; reasoning_summary: string };
|
||||
|
||||
// ─── Pending-queue card (full interactions) ───────────────────────────────────
|
||||
|
||||
function HalachaCard({
|
||||
h, focused, onApprove, onReject, onDefer, onSave,
|
||||
}: {
|
||||
@@ -82,7 +70,6 @@ function HalachaCard({
|
||||
reasoning_summary: h.reasoning_summary,
|
||||
});
|
||||
|
||||
// Reset draft when underlying row changes (focus moves to a new card).
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setDraft({
|
||||
@@ -104,7 +91,6 @@ function HalachaCard({
|
||||
${focused ? "border-gold ring-2 ring-gold/40 shadow-md" : "border-rule"}
|
||||
`}
|
||||
>
|
||||
{/* Header — status pills only (citation is in the group header) */}
|
||||
<div className="flex items-start gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||
{h.page_reference && (
|
||||
<span className="text-[0.7rem]">{h.page_reference}</span>
|
||||
@@ -129,7 +115,6 @@ function HalachaCard({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* #81 quality flags — explain why this item needs a human eye */}
|
||||
{h.quality_flags && h.quality_flags.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{h.quality_flags.map((f) => (
|
||||
@@ -142,7 +127,6 @@ function HalachaCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Side-by-side rule vs quote */}
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-ink-muted mb-1">ניסוח הכלל</div>
|
||||
@@ -166,7 +150,6 @@ function HalachaCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reasoning */}
|
||||
{(editing || h.reasoning_summary) && (
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-ink-muted mb-1">תמצית ההיגיון</div>
|
||||
@@ -184,7 +167,6 @@ function HalachaCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{h.practice_areas?.map((p) => (
|
||||
<Badge key={p} variant="outline" className="text-[0.65rem] bg-navy-soft/30 text-navy">
|
||||
@@ -198,7 +180,6 @@ function HalachaCard({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 justify-end pt-1 border-t border-rule-soft">
|
||||
{editing ? (
|
||||
<>
|
||||
@@ -239,6 +220,78 @@ function HalachaCard({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Restore card (rejected / approved tabs) ──────────────────────────────────
|
||||
|
||||
function HalachaRestoreCard({
|
||||
h,
|
||||
primaryLabel,
|
||||
primaryAction,
|
||||
secondaryLabel,
|
||||
secondaryAction,
|
||||
}: {
|
||||
h: Halacha;
|
||||
primaryLabel: string;
|
||||
primaryAction: () => void;
|
||||
secondaryLabel: string;
|
||||
secondaryAction: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-rule bg-surface p-4 space-y-3">
|
||||
<div className="flex items-start gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||
{h.page_reference && <span className="text-[0.7rem]">{h.page_reference}</span>}
|
||||
<span className="ms-auto flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-[0.65rem] tabular-nums">
|
||||
ביטחון {h.confidence.toFixed(2)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[0.65rem]">
|
||||
{ruleTypeLabel(h.rule_type)}
|
||||
</Badge>
|
||||
<CorroborationBadge halacha={h} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-ink-muted mb-1">ניסוח הכלל</div>
|
||||
<p className="text-navy font-medium leading-relaxed" dir="rtl">
|
||||
{h.rule_statement}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-ink-muted mb-1">ציטוט תומך</div>
|
||||
<blockquote className="text-ink-soft text-sm leading-relaxed border-r-2 border-gold pr-3" dir="rtl">
|
||||
“{h.supporting_quote}”
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{h.practice_areas?.map((p) => (
|
||||
<Badge key={p} variant="outline" className="text-[0.65rem] bg-navy-soft/30 text-navy">
|
||||
{practiceAreaLabel(p)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 justify-end pt-1 border-t border-rule-soft">
|
||||
<Button size="sm" variant="ghost"
|
||||
onClick={secondaryAction}
|
||||
className="text-ink-muted hover:text-navy">
|
||||
<RotateCcw className="w-3.5 h-3.5 me-1" />
|
||||
{secondaryLabel}
|
||||
</Button>
|
||||
<Button size="sm" onClick={primaryAction}
|
||||
className="bg-gold text-navy hover:bg-gold-deep">
|
||||
<Check className="w-3.5 h-3.5 me-1" />
|
||||
{primaryLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Shared group type ────────────────────────────────────────────────────────
|
||||
|
||||
type Group = {
|
||||
caseLawId: string;
|
||||
caseNumber: string;
|
||||
@@ -248,44 +301,168 @@ type Group = {
|
||||
items: Halacha[];
|
||||
};
|
||||
|
||||
export function HalachaReviewPanel() {
|
||||
function buildGroups(items: Halacha[]): Group[] {
|
||||
const map = new Map<string, Group>();
|
||||
for (const h of items) {
|
||||
const k = h.case_law_id;
|
||||
let g = map.get(k);
|
||||
if (!g) {
|
||||
g = {
|
||||
caseLawId: k,
|
||||
caseNumber: h.case_number ?? "",
|
||||
court: h.court ?? "",
|
||||
decisionDate: h.decision_date ?? null,
|
||||
precedentLevel: h.precedent_level ?? "",
|
||||
items: [],
|
||||
};
|
||||
map.set(k, g);
|
||||
}
|
||||
g.items.push(h);
|
||||
}
|
||||
for (const g of map.values()) {
|
||||
g.items.sort((a, b) => a.confidence - b.confidence);
|
||||
}
|
||||
return Array.from(map.values()).sort((a, b) => b.items.length - a.items.length);
|
||||
}
|
||||
|
||||
// ─── Restore panel (used for "rejected" and "approved" tabs) ──────────────────
|
||||
|
||||
function RestorePanel({
|
||||
status,
|
||||
primaryLabel,
|
||||
getPrimaryStatus,
|
||||
secondaryLabel,
|
||||
getSecondaryStatus,
|
||||
}: {
|
||||
status: string;
|
||||
primaryLabel: string;
|
||||
getPrimaryStatus: () => "approved" | "rejected" | "pending_review";
|
||||
secondaryLabel: string;
|
||||
getSecondaryStatus: () => "approved" | "rejected" | "pending_review";
|
||||
}) {
|
||||
const { data, isPending, error } = useHalachotByStatus(status);
|
||||
const update = useUpdateHalacha();
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const groups = useMemo<Group[]>(
|
||||
() => buildGroups(data?.items ?? []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const restore = async (h: Halacha, newStatus: "approved" | "rejected" | "pending_review") => {
|
||||
try {
|
||||
await update.mutateAsync({ id: h.id, patch: { review_status: newStatus } });
|
||||
toast.success("עודכן");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "שגיאה");
|
||||
}
|
||||
};
|
||||
|
||||
const toggleGroup = (caseLawId: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(caseLawId)) next.delete(caseLawId);
|
||||
else next.add(caseLawId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-5 text-danger text-center">
|
||||
{error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-24 w-full" />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!groups.length) {
|
||||
return (
|
||||
<div className="text-center text-ink-muted py-16">
|
||||
<p className="text-lg">אין הלכות בסטטוס זה.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{groups.map((g) => {
|
||||
const isOpen = expandedIds.has(g.caseLawId);
|
||||
return (
|
||||
<div key={g.caseLawId} className="rounded-lg border border-rule bg-surface overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleGroup(g.caseLawId)}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-4 py-3 text-right
|
||||
hover:bg-gold-wash/30 transition-colors
|
||||
${isOpen ? "bg-gold-wash/40 border-b border-rule" : ""}
|
||||
`}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="w-4 h-4 text-ink-muted shrink-0" />
|
||||
) : (
|
||||
<ChevronLeft className="w-4 h-4 text-ink-muted shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0 text-right">
|
||||
<div className="font-semibold text-navy truncate">
|
||||
{cleanCitation(g.caseNumber)}
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-ink-muted flex items-center gap-2 flex-wrap mt-0.5">
|
||||
{g.court && <span>{g.court}</span>}
|
||||
{g.decisionDate && <span>· {formatDate(g.decisionDate)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="tabular-nums">
|
||||
{g.items.length}
|
||||
</Badge>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="p-4 space-y-3 bg-rule-soft/20">
|
||||
{g.items.map((h) => (
|
||||
<HalachaRestoreCard
|
||||
key={h.id}
|
||||
h={h}
|
||||
primaryLabel={primaryLabel}
|
||||
primaryAction={() => restore(h, getPrimaryStatus())}
|
||||
secondaryLabel={secondaryLabel}
|
||||
secondaryAction={() => restore(h, getSecondaryStatus())}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Pending queue panel (main review flow) ───────────────────────────────────
|
||||
|
||||
function PendingPanel() {
|
||||
const { data, isPending, error } = useHalachotPending(500);
|
||||
const update = useUpdateHalacha();
|
||||
const batch = useBatchReviewHalachot();
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
const [focusedId, setFocusedId] = useState<string | null>(null);
|
||||
|
||||
// Group by precedent. Within each group, items sorted by confidence ascending
|
||||
// so the chair sees doubtful entries first. Groups themselves sort by item
|
||||
// count descending — biggest piles surface first.
|
||||
const groups = useMemo<Group[]>(() => {
|
||||
const map = new Map<string, Group>();
|
||||
for (const h of data?.items ?? []) {
|
||||
const k = h.case_law_id;
|
||||
let g = map.get(k);
|
||||
if (!g) {
|
||||
g = {
|
||||
caseLawId: k,
|
||||
caseNumber: h.case_number ?? "",
|
||||
court: h.court ?? "",
|
||||
decisionDate: h.decision_date ?? null,
|
||||
precedentLevel: h.precedent_level ?? "",
|
||||
items: [],
|
||||
};
|
||||
map.set(k, g);
|
||||
}
|
||||
g.items.push(h);
|
||||
}
|
||||
for (const g of map.values()) {
|
||||
g.items.sort((a, b) => a.confidence - b.confidence);
|
||||
}
|
||||
return Array.from(map.values()).sort((a, b) => b.items.length - a.items.length);
|
||||
}, [data]);
|
||||
const groups = useMemo<Group[]>(
|
||||
() => buildGroups(data?.items ?? []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const totalCount = data?.items.length ?? 0;
|
||||
|
||||
// Items the keyboard handler can navigate. Only items inside expanded
|
||||
// groups are "visible" — collapsed groups hide their items from J/K.
|
||||
const visibleItems = useMemo<Halacha[]>(() => {
|
||||
const out: Halacha[] = [];
|
||||
for (const g of groups) {
|
||||
@@ -294,8 +471,6 @@ export function HalachaReviewPanel() {
|
||||
return out;
|
||||
}, [groups, expandedIds]);
|
||||
|
||||
// If the focused item disappears (approved/rejected/group-collapsed), pick
|
||||
// a sensible neighbour so the highlight doesn't vanish silently.
|
||||
useEffect(() => {
|
||||
if (focusedId === null) return;
|
||||
if (visibleItems.some((h) => h.id === focusedId)) return;
|
||||
@@ -303,7 +478,6 @@ export function HalachaReviewPanel() {
|
||||
setFocusedId(visibleItems[0]?.id ?? null);
|
||||
}, [focusedId, visibleItems]);
|
||||
|
||||
// Scroll the focused row into view
|
||||
useEffect(() => {
|
||||
if (!focusedId) return;
|
||||
const el = document.querySelector(`[data-halacha-id="${focusedId}"]`);
|
||||
@@ -345,7 +519,6 @@ export function HalachaReviewPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
// #84 — one decision applied to a whole precedent group (one request).
|
||||
const reviewGroup = async (g: Group, status: "approved" | "rejected") => {
|
||||
try {
|
||||
const res = await batch.mutateAsync({
|
||||
@@ -366,7 +539,6 @@ export function HalachaReviewPanel() {
|
||||
next.delete(caseLawId);
|
||||
} else {
|
||||
next.add(caseLawId);
|
||||
// Auto-focus first item of the just-opened group
|
||||
const g = groups.find((x) => x.caseLawId === caseLawId);
|
||||
if (g && g.items.length) {
|
||||
setTimeout(() => setFocusedId(g.items[0].id), 0);
|
||||
@@ -376,8 +548,6 @@ export function HalachaReviewPanel() {
|
||||
});
|
||||
};
|
||||
|
||||
// Keyboard navigation — only acts when something is focused (i.e. at
|
||||
// least one group is open).
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const tag = (e.target as HTMLElement)?.tagName?.toLowerCase();
|
||||
@@ -444,10 +614,7 @@ export function HalachaReviewPanel() {
|
||||
{" "}· עריכה: <kbd className="bg-rule-soft px-1.5 rounded">E</kbd>
|
||||
</span>
|
||||
{expandedIds.size > 0 && (
|
||||
<Button
|
||||
size="sm" variant="ghost"
|
||||
onClick={() => setExpandedIds(new Set())}
|
||||
>
|
||||
<Button size="sm" variant="ghost" onClick={() => setExpandedIds(new Set())}>
|
||||
סגור הכל
|
||||
</Button>
|
||||
)}
|
||||
@@ -457,10 +624,7 @@ export function HalachaReviewPanel() {
|
||||
{groups.map((g) => {
|
||||
const isOpen = expandedIds.has(g.caseLawId);
|
||||
return (
|
||||
<div
|
||||
key={g.caseLawId}
|
||||
className="rounded-lg border border-rule bg-surface overflow-hidden"
|
||||
>
|
||||
<div key={g.caseLawId} className="rounded-lg border border-rule bg-surface overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleGroup(g.caseLawId)}
|
||||
@@ -486,17 +650,13 @@ export function HalachaReviewPanel() {
|
||||
{g.precedentLevel && <span>· {g.precedentLevel}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-gold-wash text-gold-deep border-gold/40 tabular-nums"
|
||||
>
|
||||
<Badge variant="outline" className="bg-gold-wash text-gold-deep border-gold/40 tabular-nums">
|
||||
{g.items.length} ממתינות
|
||||
</Badge>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="p-4 space-y-3 bg-rule-soft/20">
|
||||
{/* #84 — group batch actions: one decision for the whole ruling */}
|
||||
<div className="flex items-center gap-2 justify-end pb-1">
|
||||
<span className="me-auto text-[0.72rem] text-ink-muted">
|
||||
פעולה קבוצתית על {g.items.length} ההלכות:
|
||||
@@ -551,3 +711,95 @@ export function HalachaReviewPanel() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Count badge for tabs ─────────────────────────────────────────────────────
|
||||
|
||||
function useHalachaCount(status: string) {
|
||||
const { data } = useHalachotByStatus(status, 1000);
|
||||
return data?.count ?? data?.items.length ?? null;
|
||||
}
|
||||
|
||||
// ─── Main export ──────────────────────────────────────────────────────────────
|
||||
|
||||
type Tab = "pending" | "rejected" | "approved";
|
||||
|
||||
const TAB_LABELS: Record<Tab, string> = {
|
||||
pending: "ממתינות",
|
||||
rejected: "נדחו",
|
||||
approved: "אושרו",
|
||||
};
|
||||
|
||||
export function HalachaReviewPanel() {
|
||||
const [tab, setTab] = useState<Tab>("pending");
|
||||
const { data: pendingData } = useHalachotPending(500);
|
||||
const rejectedCount = useHalachaCount("rejected");
|
||||
const approvedCount = useHalachaCount("approved");
|
||||
|
||||
const pendingCount = pendingData?.items.length ?? null;
|
||||
|
||||
const counts: Record<Tab, number | null> = {
|
||||
pending: pendingCount,
|
||||
rejected: rejectedCount,
|
||||
approved: approvedCount,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Tab bar */}
|
||||
<div className="flex gap-1 border-b border-rule pb-0">
|
||||
{(["pending", "rejected", "approved"] as Tab[]).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => setTab(t)}
|
||||
className={`
|
||||
px-4 py-2 text-sm font-medium rounded-t-md transition-colors
|
||||
${tab === t
|
||||
? "bg-surface border border-b-surface border-rule text-navy -mb-px"
|
||||
: "text-ink-muted hover:text-navy hover:bg-rule-soft/40"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{TAB_LABELS[t]}
|
||||
{counts[t] !== null && counts[t]! > 0 && (
|
||||
<span className={`
|
||||
ms-2 text-[0.7rem] px-1.5 py-0.5 rounded-full tabular-nums
|
||||
${t === "pending"
|
||||
? "bg-gold-wash text-gold-deep"
|
||||
: t === "rejected"
|
||||
? "bg-danger-bg text-danger"
|
||||
: "bg-navy-soft/20 text-navy"
|
||||
}
|
||||
`}>
|
||||
{counts[t]}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
{tab === "pending" && <PendingPanel />}
|
||||
|
||||
{tab === "rejected" && (
|
||||
<RestorePanel
|
||||
status="rejected"
|
||||
primaryLabel="אשר"
|
||||
getPrimaryStatus={() => "approved"}
|
||||
secondaryLabel="שחזר לתור"
|
||||
getSecondaryStatus={() => "pending_review"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "approved" && (
|
||||
<RestorePanel
|
||||
status="approved"
|
||||
primaryLabel="בטל אישור"
|
||||
getPrimaryStatus={() => "pending_review"}
|
||||
secondaryLabel="דחה"
|
||||
getSecondaryStatus={() => "rejected"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
|
||||
<Label htmlFor="chair-name">יו"ר</Label>
|
||||
<Input id="chair-name" value={form.chair_name}
|
||||
onChange={(e) => setForm({ ...form, chair_name: e.target.value })}
|
||||
placeholder="עו״ד דפנה תמיר" />
|
||||
placeholder="" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="date">תאריך</Label>
|
||||
|
||||
@@ -576,6 +576,19 @@ export function useHalachotPending(limit = 200) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useHalachotByStatus(status: string, limit = 300) {
|
||||
return useQuery({
|
||||
queryKey: libraryKeys.halachot({ review_status: status, limit: String(limit) }),
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<{ items: Halacha[]; count: number }>(
|
||||
`/api/halachot?review_status=${encodeURIComponent(status)}&limit=${limit}`,
|
||||
{ signal },
|
||||
),
|
||||
staleTime: 10_000,
|
||||
refetchOnMount: "always",
|
||||
});
|
||||
}
|
||||
|
||||
export type HalachaPatch = Partial<{
|
||||
review_status: "pending_review" | "approved" | "rejected" | "published" | "deferred";
|
||||
reviewer: string;
|
||||
|
||||
Reference in New Issue
Block a user