feat(ui): פאנל אישור-תכניות — טאב /precedents + מרכז-אישורים (PR-B) #254
@@ -7,8 +7,10 @@ import { AppShell } from "@/components/app-shell";
|
|||||||
import { LibraryListPanel } from "@/components/precedents/library-list-panel";
|
import { LibraryListPanel } from "@/components/precedents/library-list-panel";
|
||||||
import { LibrarySearchPanel } from "@/components/precedents/library-search-panel";
|
import { LibrarySearchPanel } from "@/components/precedents/library-search-panel";
|
||||||
import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel";
|
import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel";
|
||||||
|
import { PlansReviewPanel } from "@/components/precedents/plans-review-panel";
|
||||||
import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel";
|
import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel";
|
||||||
import { useHalachotPending } from "@/lib/api/precedent-library";
|
import { useHalachotPending } from "@/lib/api/precedent-library";
|
||||||
|
import { usePlansPending } from "@/lib/api/plans";
|
||||||
import { useMissingPrecedents } from "@/lib/api/missing-precedents";
|
import { useMissingPrecedents } from "@/lib/api/missing-precedents";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,13 +49,18 @@ function PendingPill() {
|
|||||||
return <CountPill n={data?.count ?? 0} tone="warn" />;
|
return <CountPill n={data?.count ?? 0} tone="warn" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PlansPendingPill() {
|
||||||
|
const { data } = usePlansPending();
|
||||||
|
return <CountPill n={data?.count ?? 0} tone="warn" />;
|
||||||
|
}
|
||||||
|
|
||||||
function IncomingPill() {
|
function IncomingPill() {
|
||||||
// "פסיקה נכנסת" = open missing-precedents waiting for the chair to upload.
|
// "פסיקה נכנסת" = open missing-precedents waiting for the chair to upload.
|
||||||
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
|
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
|
||||||
return <CountPill n={data?.by_status?.open ?? 0} tone="info" />;
|
return <CountPill n={data?.by_status?.open ?? 0} tone="info" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRECEDENT_TABS = new Set(["library", "search", "review", "incoming", "stats"]);
|
const PRECEDENT_TABS = new Set(["library", "search", "review", "plans", "incoming", "stats"]);
|
||||||
|
|
||||||
export default function PrecedentsPage() {
|
export default function PrecedentsPage() {
|
||||||
// Controlled so a deep link like /precedents?tab=review (e.g. from a pending
|
// Controlled so a deep link like /precedents?tab=review (e.g. from a pending
|
||||||
@@ -92,6 +99,7 @@ export default function PrecedentsPage() {
|
|||||||
{ value: "library", label: "ספרייה", pill: null },
|
{ value: "library", label: "ספרייה", pill: null },
|
||||||
{ value: "search", label: "חיפוש בקורפוס", pill: null },
|
{ value: "search", label: "חיפוש בקורפוס", pill: null },
|
||||||
{ value: "review", label: "תור הלכות", pill: <PendingPill /> },
|
{ value: "review", label: "תור הלכות", pill: <PendingPill /> },
|
||||||
|
{ value: "plans", label: "תכניות", pill: <PlansPendingPill /> },
|
||||||
{
|
{
|
||||||
value: "incoming",
|
value: "incoming",
|
||||||
label: "פסיקה נכנסת",
|
label: "פסיקה נכנסת",
|
||||||
@@ -123,6 +131,10 @@ export default function PrecedentsPage() {
|
|||||||
<HalachaReviewPanel />
|
<HalachaReviewPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="plans" className="mt-0">
|
||||||
|
<PlansReviewPanel />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{/* "פסיקה נכנסת" — the incoming/missing-precedent queue. Kept as a
|
{/* "פסיקה נכנסת" — the incoming/missing-precedent queue. Kept as a
|
||||||
tab per the mockup; full management lives on /missing-precedents. */}
|
tab per the mockup; full management lives on /missing-precedents. */}
|
||||||
<TabsContent value="incoming" className="mt-0">
|
<TabsContent value="incoming" className="mt-0">
|
||||||
|
|||||||
358
web-ui/src/components/precedents/plans-review-panel.tsx
Normal file
358
web-ui/src/components/precedents/plans-review-panel.tsx
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Check, X, Edit2, AlertTriangle, Plus, GitMerge, Save } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import {
|
||||||
|
usePlansPending, usePlanDuplicates, useUpsertPlan, useUpdatePlan,
|
||||||
|
useReviewPlan, useMergePlans, type Plan, type PlanEdit,
|
||||||
|
} from "@/lib/api/plans";
|
||||||
|
|
||||||
|
/* Strip bidi marks (mirror of the halacha panel's cleanCitation). */
|
||||||
|
function clean(s: string | null | undefined): string {
|
||||||
|
if (!s) return "—";
|
||||||
|
return s.replace(/[--]/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(iso: string | null | undefined): string {
|
||||||
|
if (!iso) return "—";
|
||||||
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
|
||||||
|
if (!m) return iso;
|
||||||
|
return `${Number(m[3])}.${Number(m[2])}.${m[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditForm = {
|
||||||
|
plan_number: string;
|
||||||
|
display_name: string;
|
||||||
|
plan_type: string;
|
||||||
|
gazette_date: string;
|
||||||
|
yalkut_number: string;
|
||||||
|
purpose: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLANK: EditForm = {
|
||||||
|
plan_number: "", display_name: "", plan_type: "",
|
||||||
|
gazette_date: "", yalkut_number: "", purpose: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function toForm(p: Plan): EditForm {
|
||||||
|
return {
|
||||||
|
plan_number: p.plan_number,
|
||||||
|
display_name: p.display_name,
|
||||||
|
plan_type: p.plan_type,
|
||||||
|
gazette_date: p.gazette_date ?? "",
|
||||||
|
yalkut_number: p.yalkut_number,
|
||||||
|
purpose: p.purpose,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Client-side mirror of db.format_plan_citation — deterministic preview so the
|
||||||
|
* chair sees exactly what block-tet will cite (validity is never free-typed). */
|
||||||
|
function previewCitation(f: EditForm): string {
|
||||||
|
const name = f.display_name.trim() || f.plan_number.trim();
|
||||||
|
if (!name) return "—";
|
||||||
|
let s = name;
|
||||||
|
const d = fmtDate(f.gazette_date);
|
||||||
|
if (d !== "—") {
|
||||||
|
s = `${name} פורסמה למתן תוקף ברשומות ביום ${d}`;
|
||||||
|
if (f.yalkut_number.trim()) s += `, י"פ ${f.yalkut_number.trim()}`;
|
||||||
|
}
|
||||||
|
if (f.purpose.trim()) s += ` — ${f.purpose.trim()}`;
|
||||||
|
return s.endsWith(".") ? s : s + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function PlansReviewPanel() {
|
||||||
|
const { data, isLoading, isError } = usePlansPending();
|
||||||
|
const [adding, setAdding] = useState(false);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[0, 1, 2].map((i) => <Skeleton key={i} className="h-40 w-full rounded-lg" />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isError) {
|
||||||
|
return <p className="text-danger text-sm">שגיאה בטעינת תור-התכניות.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plans = data?.items ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
|
<p className="text-ink-soft text-sm leading-relaxed max-w-3xl">
|
||||||
|
מרשם-התכניות: זהות + תוקף קנוניים של כל תב"ע, בשימוש חוזר בין תיקים.
|
||||||
|
רק תכנית מאושרת מצוטטת בבלוק ט; התוקף נכתב בנוסח אחיד{" "}
|
||||||
|
<span className="text-gold-deep font-semibold">דטרמיניסטי</span> —
|
||||||
|
תאריך-הרשומות ומס' הילקוט לעולם אינם מומצאים.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setAdding((v) => !v)}
|
||||||
|
className="bg-navy text-white hover:bg-navy-soft shrink-0"
|
||||||
|
>
|
||||||
|
<Plus className="size-4" /> הוספת תכנית ידנית
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{adding && (
|
||||||
|
<PlanForm
|
||||||
|
title="הוספת תכנית"
|
||||||
|
subtitle="מספר-התכנית מנורמל אוטומטית. ברירת-מחדל: מאושר (קלט-יו״ר)."
|
||||||
|
initial={BLANK}
|
||||||
|
mode="add"
|
||||||
|
onClose={() => setAdding(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plans.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-rule bg-surface shadow-sm p-6 text-center text-ink-muted text-sm">
|
||||||
|
אין תכניות הממתינות לאישור.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
plans.map((p) => <PlanCard key={p.id} plan={p} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── one plan card ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function PlanCard({ plan }: { plan: Plan }) {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const review = useReviewPlan();
|
||||||
|
const merge = useMergePlans();
|
||||||
|
const { data: dups } = usePlanDuplicates(plan.id);
|
||||||
|
const duplicates = dups?.items ?? [];
|
||||||
|
|
||||||
|
const noDate = !plan.gazette_date;
|
||||||
|
|
||||||
|
async function decide(status: "approved" | "rejected") {
|
||||||
|
try {
|
||||||
|
await review.mutateAsync({ id: plan.id, status });
|
||||||
|
toast.success(status === "approved" ? "התכנית אושרה" : "התכנית נדחתה");
|
||||||
|
} catch {
|
||||||
|
toast.error("שגיאה בעדכון התכנית");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function foldIn(sourceId: string, label: string) {
|
||||||
|
try {
|
||||||
|
await merge.mutateAsync({ sourceId, targetId: plan.id });
|
||||||
|
toast.success(`מוזג: ${label} → ${plan.display_name || plan.plan_number}`);
|
||||||
|
} catch {
|
||||||
|
toast.error("שגיאה במיזוג");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
return (
|
||||||
|
<PlanForm
|
||||||
|
title={`עריכת תכנית — ${plan.display_name || plan.plan_number}`}
|
||||||
|
subtitle="שינוי מספר-התכנית מנורמל אוטומטית; אם הוא מתנגש בתכנית קיימת — תופנה למיזוג."
|
||||||
|
initial={toForm(plan)}
|
||||||
|
mode="edit"
|
||||||
|
planId={plan.id}
|
||||||
|
onClose={() => setEditing(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-rule bg-gold-wash shadow-sm p-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Badge className="bg-gold text-white rounded">תכנית</Badge>
|
||||||
|
<span className="text-navy font-bold text-[0.95rem]">
|
||||||
|
{plan.display_name || plan.plan_number}
|
||||||
|
</span>
|
||||||
|
{plan.plan_type && (
|
||||||
|
<span className="rounded-full bg-info-bg text-[#365070] text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||||
|
{plan.plan_type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{plan.source_case_number && (
|
||||||
|
<span className="ms-auto text-ink-muted text-[0.72rem]">
|
||||||
|
מקור: {plan.source_case_number}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{duplicates.length > 0 && (
|
||||||
|
<div className="rounded-md border border-[#e8d3a8] bg-warn-bg px-3 py-2 space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2 text-[0.78rem] text-gold-deep">
|
||||||
|
<AlertTriangle className="size-4 shrink-0" />
|
||||||
|
<span>
|
||||||
|
כפילות אפשרית — ייתכן שאותה תכנית כבר במרשם בצורה אחרת. מזג כדי לא
|
||||||
|
לפצל תכנית אחת לשתי רשומות.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{duplicates.map((d) => (
|
||||||
|
<div key={d.id} className="flex items-center gap-2 flex-wrap ps-6">
|
||||||
|
<span className="text-[0.78rem] text-navy font-medium">
|
||||||
|
{d.display_name || d.plan_number}
|
||||||
|
</span>
|
||||||
|
{d.match_reason && (
|
||||||
|
<span className="text-[0.68rem] text-ink-muted">({d.match_reason})</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm" variant="outline"
|
||||||
|
className="ms-auto h-7 border-gold text-gold-deep text-[0.72rem]"
|
||||||
|
disabled={merge.isPending}
|
||||||
|
onClick={() => foldIn(d.id, d.display_name || d.plan_number)}
|
||||||
|
>
|
||||||
|
<GitMerge className="size-3.5" /> מזג לכאן
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-[0.72rem] text-ink-muted font-semibold mb-1.5">
|
||||||
|
משפט-הציטוט הקנוני (כפי שייכתב בבלוק ט)
|
||||||
|
</div>
|
||||||
|
<div className="border-s-[3px] border-gold bg-white/65 rounded-e px-3.5 py-2.5 text-[0.92rem] leading-relaxed font-medium">
|
||||||
|
{clean(plan.citation_formatted)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-x-6 gap-y-1 text-[0.78rem] text-ink-soft">
|
||||||
|
<div><span className="text-ink-muted">תאריך רשומות:</span> {fmtDate(plan.gazette_date)}</div>
|
||||||
|
<div><span className="text-ink-muted">ילקוט פרסומים:</span> {plan.yalkut_number || "—"}</div>
|
||||||
|
{plan.plan_type && <div><span className="text-ink-muted">סוג:</span> {plan.plan_type}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{noDate && (
|
||||||
|
<div className="text-[0.78rem] text-danger font-medium">
|
||||||
|
⚠ חסר תאריך-תוקף — תצוטט ללא תוקף עד השלמה.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-wrap justify-end pt-2.5 border-t border-rule-soft">
|
||||||
|
<Button
|
||||||
|
variant="ghost" size="sm" className="me-auto text-ink-muted"
|
||||||
|
onClick={() => setEditing(true)}
|
||||||
|
>
|
||||||
|
<Edit2 className="size-4" /> {noDate ? "השלם תוקף" : "ערוך / תקן"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline" size="sm" className="border-[#e3c4c4] text-danger"
|
||||||
|
disabled={review.isPending}
|
||||||
|
onClick={() => decide("rejected")}
|
||||||
|
>
|
||||||
|
<X className="size-4" /> דחה
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm" className="bg-gold text-navy hover:bg-gold-deep hover:text-white"
|
||||||
|
disabled={review.isPending}
|
||||||
|
onClick={() => decide("approved")}
|
||||||
|
>
|
||||||
|
<Check className="size-4" /> {noDate ? "אשר (ללא תוקף)" : "אשר"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── add / edit form (inline) ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
function PlanForm({
|
||||||
|
title, subtitle, initial, mode, planId, onClose,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
initial: EditForm;
|
||||||
|
mode: "add" | "edit";
|
||||||
|
planId?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [f, setF] = useState<EditForm>(initial);
|
||||||
|
const upsert = useUpsertPlan();
|
||||||
|
const update = useUpdatePlan();
|
||||||
|
const busy = upsert.isPending || update.isPending;
|
||||||
|
|
||||||
|
function set<K extends keyof EditForm>(k: K, v: string) {
|
||||||
|
setF((prev) => ({ ...prev, [k]: v }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!f.plan_number.trim()) {
|
||||||
|
toast.error("חובה מספר-תכנית");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (mode === "edit" && planId) {
|
||||||
|
const patch: PlanEdit = {
|
||||||
|
plan_number: f.plan_number, display_name: f.display_name,
|
||||||
|
plan_type: f.plan_type, gazette_date: f.gazette_date,
|
||||||
|
yalkut_number: f.yalkut_number, purpose: f.purpose,
|
||||||
|
};
|
||||||
|
await update.mutateAsync({ id: planId, patch });
|
||||||
|
} else {
|
||||||
|
await upsert.mutateAsync({
|
||||||
|
plan_number: f.plan_number, display_name: f.display_name,
|
||||||
|
plan_type: f.plan_type, gazette_date: f.gazette_date,
|
||||||
|
yalkut_number: f.yalkut_number, purpose: f.purpose,
|
||||||
|
review_status: "approved",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toast.success(mode === "edit" ? "התכנית נשמרה" : "התכנית נוספה");
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
// surface the backend's collision/validation message (no silent failure)
|
||||||
|
const msg = (e as { body?: { detail?: string } })?.body?.detail;
|
||||||
|
toast.error(msg || "שגיאה בשמירה");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = (
|
||||||
|
k: keyof EditForm, label: string, hint?: string, full = false,
|
||||||
|
) => (
|
||||||
|
<div className={full ? "col-span-2" : ""}>
|
||||||
|
<Label className="text-[0.72rem] text-ink-muted mb-1 block">{label}</Label>
|
||||||
|
<Input value={f[k]} onChange={(e) => set(k, e.target.value)} />
|
||||||
|
{hint && <p className="text-[0.66rem] text-ink-muted mt-1">{hint}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-rule bg-surface shadow-sm p-4 space-y-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-navy text-sm font-bold m-0">{title}</h3>
|
||||||
|
<p className="text-ink-muted text-xs mt-0.5">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{field("plan_number", "מספר תכנית", 'מזהה בלבד (ללא המילה "תכנית")')}
|
||||||
|
{field("display_name", "שם תצוגה")}
|
||||||
|
{field("gazette_date", "תאריך פרסום ברשומות", "YYYY-MM-DD")}
|
||||||
|
{field("yalkut_number", 'מס\' ילקוט פרסומים (י"פ)')}
|
||||||
|
{field("plan_type", "סוג תכנית", "ארצית / מחוזית / מקומית / מפורטת / כוללנית")}
|
||||||
|
{field("purpose", "ייעוד (משפט אחד)", undefined, true)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-dashed border-gold bg-gold-wash px-3.5 py-2.5">
|
||||||
|
<span className="block text-[0.66rem] text-gold-deep font-bold mb-1">
|
||||||
|
תצוגה מקדימה — כך ייכתב בבלוק ט:
|
||||||
|
</span>
|
||||||
|
<span className="text-[0.88rem] leading-relaxed">{previewCitation(f)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 justify-end">
|
||||||
|
<Button variant="ghost" size="sm" className="me-auto text-ink-muted" onClick={onClose}>
|
||||||
|
ביטול
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm" className="bg-gold text-navy hover:bg-gold-deep hover:text-white"
|
||||||
|
disabled={busy} onClick={save}
|
||||||
|
>
|
||||||
|
<Save className="size-4" /> שמור
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
web-ui/src/lib/api/plans.ts
Normal file
152
web-ui/src/lib/api/plans.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Planning-schemes registry (מרשם-התכניות, V38) — typed API hooks.
|
||||||
|
*
|
||||||
|
* SSOT for a plan's identity + validity, reused across cases. LLM-extracted rows
|
||||||
|
* arrive pending_review; only approved validity feeds block-tet (INV-DM5/G10).
|
||||||
|
* Variant duplicates are surfaced (usePlanDuplicates), never auto-merged — the
|
||||||
|
* chair merges manually. Mirrors the precedent-library hook conventions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { apiRequest } from "./client";
|
||||||
|
|
||||||
|
export type PlanReviewStatus = "pending_review" | "approved" | "rejected";
|
||||||
|
|
||||||
|
export type PlanDiscrepancy = {
|
||||||
|
field: string;
|
||||||
|
old: string;
|
||||||
|
new: string;
|
||||||
|
source_case_number?: string;
|
||||||
|
via?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Plan = {
|
||||||
|
id: string;
|
||||||
|
plan_number: string;
|
||||||
|
display_name: string;
|
||||||
|
aliases: string[];
|
||||||
|
plan_type: string;
|
||||||
|
gazette_date: string | null; // ISO YYYY-MM-DD
|
||||||
|
yalkut_number: string;
|
||||||
|
purpose: string;
|
||||||
|
citation_formatted: string; // the deterministic block-tet sentence
|
||||||
|
review_status: PlanReviewStatus;
|
||||||
|
source_case_number: string;
|
||||||
|
source_document_id: string | null;
|
||||||
|
model_used: string;
|
||||||
|
discrepancies: PlanDiscrepancy[];
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
/** Only present on a duplicate-candidate hit. */
|
||||||
|
match_reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const planKeys = {
|
||||||
|
all: ["plans"] as const,
|
||||||
|
pending: () => [...planKeys.all, "pending"] as const,
|
||||||
|
duplicates: (id: string) => [...planKeys.all, "duplicates", id] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** All plans awaiting the chair gate (G10). */
|
||||||
|
export function usePlansPending(limit = 500) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planKeys.pending(),
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
apiRequest<{ items: Plan[]; count: number }>(
|
||||||
|
`/api/plans?review_status=pending_review&limit=${limit}`,
|
||||||
|
{ signal },
|
||||||
|
),
|
||||||
|
staleTime: 5_000,
|
||||||
|
refetchOnMount: "always",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Near-duplicate candidates for a plan — surfaced for manual merge. */
|
||||||
|
export function usePlanDuplicates(planId: string, enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planKeys.duplicates(planId),
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
apiRequest<{ items: Plan[]; count: number }>(
|
||||||
|
`/api/plans/${encodeURIComponent(planId)}/duplicates`,
|
||||||
|
{ signal },
|
||||||
|
),
|
||||||
|
enabled: enabled && !!planId,
|
||||||
|
staleTime: 10_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlanUpsert = {
|
||||||
|
plan_number: string;
|
||||||
|
display_name?: string;
|
||||||
|
plan_type?: string;
|
||||||
|
gazette_date?: string; // ISO; "" = unknown
|
||||||
|
yalkut_number?: string;
|
||||||
|
purpose?: string;
|
||||||
|
review_status?: PlanReviewStatus;
|
||||||
|
aliases?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlanEdit = {
|
||||||
|
plan_number: string;
|
||||||
|
display_name?: string;
|
||||||
|
plan_type?: string;
|
||||||
|
gazette_date?: string;
|
||||||
|
yalkut_number?: string;
|
||||||
|
purpose?: string;
|
||||||
|
aliases?: string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function invalidate(qc: ReturnType<typeof useQueryClient>) {
|
||||||
|
qc.invalidateQueries({ queryKey: planKeys.all });
|
||||||
|
// the chair-pending aggregate (/approvals) carries a plans count (INV-IA2)
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Manual chair add/upsert (idempotent on normalized plan_number). */
|
||||||
|
export function useUpsertPlan() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: PlanUpsert) =>
|
||||||
|
apiRequest<Plan>(`/api/plans`, { method: "POST", body }),
|
||||||
|
onSuccess: () => invalidate(qc),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Edit/fix a plan by id (refuses a number collision → use merge). */
|
||||||
|
export function useUpdatePlan() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, patch }: { id: string; patch: PlanEdit }) =>
|
||||||
|
apiRequest<Plan>(
|
||||||
|
`/api/plans/${encodeURIComponent(id)}`,
|
||||||
|
{ method: "PATCH", body: patch },
|
||||||
|
),
|
||||||
|
onSuccess: () => invalidate(qc),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Chair gate (G10): approve / reject / reset. */
|
||||||
|
export function useReviewPlan() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, status }: { id: string; status: PlanReviewStatus }) =>
|
||||||
|
apiRequest<Plan>(
|
||||||
|
`/api/plans/${encodeURIComponent(id)}/review`,
|
||||||
|
{ method: "POST", body: { review_status: status } },
|
||||||
|
),
|
||||||
|
onSuccess: () => invalidate(qc),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fold a source plan into a target (manual dedup); source is deleted. */
|
||||||
|
export function useMergePlans() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ sourceId, targetId }: { sourceId: string; targetId: string }) =>
|
||||||
|
apiRequest<Plan>(
|
||||||
|
`/api/plans/merge`,
|
||||||
|
{ method: "POST", body: { source_id: sourceId, target_id: targetId } },
|
||||||
|
),
|
||||||
|
onSuccess: () => invalidate(qc),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2947,9 +2947,11 @@ export interface paths {
|
|||||||
* Operations Drain Toggle
|
* Operations Drain Toggle
|
||||||
* @description Switch a cron drain on/off (the 'startup type' in the services panel).
|
* @description Switch a cron drain on/off (the 'startup type' in the services panel).
|
||||||
*
|
*
|
||||||
* Written straight to drain_controls — no host roundtrip; the drain reads the
|
* Written to drain_controls so the drain no-ops at its NEXT startup (pm2
|
||||||
* flag at startup and no-ops when disabled (pm2 cron_restart can't be trusted
|
* cron_restart can't be trusted to stay stopped). On disable we ALSO stop any
|
||||||
* to stay stopped).
|
* currently-running process immediately via the host pm2 bridge — the DB flag
|
||||||
|
* alone wouldn't halt a drain mid-run. Best-effort: a bridge failure doesn't
|
||||||
|
* fail the toggle (the supervisor stops it on its next tick as a backstop).
|
||||||
*/
|
*/
|
||||||
post: operations["operations_drain_toggle_api_operations_drains__name__disabled_post"];
|
post: operations["operations_drain_toggle_api_operations_drains__name__disabled_post"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -3087,6 +3089,33 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/operations/agents/migrate-adapter": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/**
|
||||||
|
* Operations Agent Migrate Adapter
|
||||||
|
* @description Migrate an agent (or 'all') to a target adapter, in both companies.
|
||||||
|
*
|
||||||
|
* The migration is host-side (it needs the host filesystem — generated
|
||||||
|
* instruction copies, the gemini settings file — and the embedded board DB),
|
||||||
|
* so this proxies scripts/migrate_agent_adapter.py through the court-fetch host
|
||||||
|
* bridge, Bearer-authenticated exactly like the pm2 controls. The script's exit
|
||||||
|
* code + stdout/stderr are relayed verbatim so the dashboard can show preflight
|
||||||
|
* warnings (a non-zero --check is a refusal to render, not a transport error).
|
||||||
|
*/
|
||||||
|
post: operations["operations_agent_migrate_adapter_api_operations_agents_migrate_adapter_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/digests/{digest_id}": {
|
"/api/digests/{digest_id}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -3428,6 +3457,111 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/plans": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Plans List
|
||||||
|
* @description List the plans registry; filter by review_status (queue) or fuzzy q (search).
|
||||||
|
*/
|
||||||
|
get: operations["plans_list_api_plans_get"];
|
||||||
|
put?: never;
|
||||||
|
/**
|
||||||
|
* Plan Create
|
||||||
|
* @description Manual chair add/upsert (idempotent on normalized plan_number).
|
||||||
|
*/
|
||||||
|
post: operations["plan_create_api_plans_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/plans/{plan_id}/duplicates": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Plan Duplicates
|
||||||
|
* @description Near-duplicate candidates for a plan — for the chair to merge (G10, no auto-merge).
|
||||||
|
*/
|
||||||
|
get: operations["plan_duplicates_api_plans__plan_id__duplicates_get"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/plans/{plan_id}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** Plan Get */
|
||||||
|
get: operations["plan_get_api_plans__plan_id__get"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
/**
|
||||||
|
* Plan Edit
|
||||||
|
* @description Edit/fix a plan by id (chair). Refuses a number collision (→ merge instead).
|
||||||
|
*/
|
||||||
|
patch: operations["plan_edit_api_plans__plan_id__patch"];
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/plans/{plan_id}/review": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/**
|
||||||
|
* Plan Review
|
||||||
|
* @description Chair gate (G10): approve / reject / reset.
|
||||||
|
*/
|
||||||
|
post: operations["plan_review_api_plans__plan_id__review_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/plans/merge": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/**
|
||||||
|
* Plan Merge
|
||||||
|
* @description Fold source plan into target (chair-initiated dedup); source is deleted.
|
||||||
|
*/
|
||||||
|
post: operations["plan_merge_api_plans_merge_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/missing-precedents": {
|
"/api/missing-precedents": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -3556,6 +3690,22 @@ export interface paths {
|
|||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
/** AdapterMigrationRequest */
|
||||||
|
AdapterMigrationRequest: {
|
||||||
|
/** Action */
|
||||||
|
action: string;
|
||||||
|
/** Agent */
|
||||||
|
agent?: string | null;
|
||||||
|
/** To */
|
||||||
|
to?: string | null;
|
||||||
|
/** Model */
|
||||||
|
model?: string | null;
|
||||||
|
/**
|
||||||
|
* Relax Tools
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
relax_tools: boolean;
|
||||||
|
};
|
||||||
/** AgentCommentRequest */
|
/** AgentCommentRequest */
|
||||||
AgentCommentRequest: {
|
AgentCommentRequest: {
|
||||||
/** Body */
|
/** Body */
|
||||||
@@ -4428,6 +4578,90 @@ export interface components {
|
|||||||
*/
|
*/
|
||||||
appeal_type: string;
|
appeal_type: string;
|
||||||
};
|
};
|
||||||
|
/** PlanEditRequest */
|
||||||
|
PlanEditRequest: {
|
||||||
|
/** Plan Number */
|
||||||
|
plan_number: string;
|
||||||
|
/**
|
||||||
|
* Display Name
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
display_name: string;
|
||||||
|
/**
|
||||||
|
* Plan Type
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
plan_type: string;
|
||||||
|
/**
|
||||||
|
* Gazette Date
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
gazette_date: string;
|
||||||
|
/**
|
||||||
|
* Yalkut Number
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
yalkut_number: string;
|
||||||
|
/**
|
||||||
|
* Purpose
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
purpose: string;
|
||||||
|
/** Aliases */
|
||||||
|
aliases?: string[] | null;
|
||||||
|
};
|
||||||
|
/** PlanMergeRequest */
|
||||||
|
PlanMergeRequest: {
|
||||||
|
/** Source Id */
|
||||||
|
source_id: string;
|
||||||
|
/** Target Id */
|
||||||
|
target_id: string;
|
||||||
|
};
|
||||||
|
/** PlanReviewRequest */
|
||||||
|
PlanReviewRequest: {
|
||||||
|
/** Review Status */
|
||||||
|
review_status: string;
|
||||||
|
};
|
||||||
|
/** PlanUpsertRequest */
|
||||||
|
PlanUpsertRequest: {
|
||||||
|
/** Plan Number */
|
||||||
|
plan_number: string;
|
||||||
|
/**
|
||||||
|
* Display Name
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
display_name: string;
|
||||||
|
/**
|
||||||
|
* Plan Type
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
plan_type: string;
|
||||||
|
/**
|
||||||
|
* Gazette Date
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
gazette_date: string;
|
||||||
|
/**
|
||||||
|
* Yalkut Number
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
yalkut_number: string;
|
||||||
|
/**
|
||||||
|
* Purpose
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
purpose: string;
|
||||||
|
/**
|
||||||
|
* Review Status
|
||||||
|
* @default approved
|
||||||
|
*/
|
||||||
|
review_status: string;
|
||||||
|
/**
|
||||||
|
* Aliases
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
aliases: string[];
|
||||||
|
};
|
||||||
/** PrecedentCreateRequest */
|
/** PrecedentCreateRequest */
|
||||||
PrecedentCreateRequest: {
|
PrecedentCreateRequest: {
|
||||||
/** Quote */
|
/** Quote */
|
||||||
@@ -9354,6 +9588,39 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
operations_agent_migrate_adapter_api_operations_agents_migrate_adapter_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["AdapterMigrationRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
digest_get_api_digests__digest_id__get: {
|
digest_get_api_digests__digest_id__get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -10016,6 +10283,237 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
plans_list_api_plans_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
review_status?: string;
|
||||||
|
q?: string;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
plan_create_api_plans_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["PlanUpsertRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
plan_duplicates_api_plans__plan_id__duplicates_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
plan_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
plan_get_api_plans__plan_id__get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
plan_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
plan_edit_api_plans__plan_id__patch: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
plan_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["PlanEditRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
plan_review_api_plans__plan_id__review_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
plan_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["PlanReviewRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
plan_merge_api_plans_merge_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["PlanMergeRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
missing_precedents_list_api_missing_precedents_get: {
|
missing_precedents_list_api_missing_precedents_get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
|
|||||||
@@ -5786,7 +5786,7 @@ async def api_chair_pending():
|
|||||||
"key": "plans", "label": "תכניות הממתינות לאישור",
|
"key": "plans", "label": "תכניות הממתינות לאישור",
|
||||||
"description": "תכניות שחולצו אוטומטית מהחלטות — מצוטטות בבלוק ט רק לאחר אישורך.",
|
"description": "תכניות שחולצו אוטומטית מהחלטות — מצוטטות בבלוק ט רק לאחר אישורך.",
|
||||||
"count": pl_count, "severity": "medium" if pl_count else "ok",
|
"count": pl_count, "severity": "medium" if pl_count else "ok",
|
||||||
"href": "/precedents", "oldest_at": pl_oldest.isoformat() if pl_oldest else None,
|
"href": "/precedents?tab=plans", "oldest_at": pl_oldest.isoformat() if pl_oldest else None,
|
||||||
"sample": [{"text": r["name"], "source": (r["purpose"] or "")[:80]} for r in pl_sample],
|
"sample": [{"text": r["name"], "source": (r["purpose"] or "")[:80]} for r in pl_sample],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user