feat(plans): כפתור "משוך מ-מנהל-התכנון" בטופס-התכנית (Phase C טריגר 1)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s

טריגר 1 הידני: בטופס PlanForm, כפתור "משוך מ-מנהל-התכנון" — היו"ר
מקליד מספר-תכנית, לוחץ, והשדות (שם/תאריך-רשומות/י"פ/סוג/ייעוד)
מתמלאים מ-mavat דרך POST /api/plans/fetch (#292). היו"ר בודק ושומר —
שער-היו"ר נשמר (שום שמירה אוטומטית).

- plans.ts: useFetchPlan + PlanFetchResult.
- PlanForm: כפתור עם spinner (~דקה, דפדפן חי), מילוי-שדות (מחליף
  בערך-mavat היכן שקיים, שומר ערך-יו"ר היכן ש-mavat ריק), קישור-מקור
  "מקור: מנהל-התכנון" בתצוגה-המקדימה (פרובננס INV-AH).

עבר שער-עיצוב (מוקאפ 22-plans-review מאושר). ההוק ידני (לא תלוי
types שנוצרים). tsc  lint  (0 errors).

INV-AH: source_url מוצג; שדה-חסר ריק לא מומצא. G10: מילוי-טופס בלבד,
שמירה דרך plan_upsert הקיים. G2: צורך את /api/plans/fetch (#292).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:32:32 +00:00
parent 4994ae0cba
commit 2b1fb18dfd
2 changed files with 91 additions and 1 deletions

View File

@@ -3,6 +3,7 @@
import { useMemo, useState } from "react";
import {
Check, X, Edit2, AlertTriangle, Plus, GitMerge, Save, Search, Undo2, BadgeCheck,
DownloadCloud, Loader2, ExternalLink,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -13,7 +14,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import {
usePlansAll, usePlanDuplicates, useUpsertPlan, useUpdatePlan,
useReviewPlan, useMergePlans, type Plan, type PlanEdit,
useReviewPlan, useMergePlans, useFetchPlan, type Plan, type PlanEdit,
} from "@/lib/api/plans";
/* Strip bidi marks (mirror of the halacha panel's cleanCitation). */
@@ -401,14 +402,47 @@ function PlanForm({
onClose: () => void;
}) {
const [f, setF] = useState<EditForm>(initial);
const [sourceUrl, setSourceUrl] = useState("");
const upsert = useUpsertPlan();
const update = useUpdatePlan();
const fetchMavat = useFetchPlan();
const busy = upsert.isPending || update.isPending;
function set<K extends keyof EditForm>(k: K, v: string) {
setF((prev) => ({ ...prev, [k]: v }));
}
// Pull identity + validity from the official source (mavat) into the fields.
// Fills each field mavat returns (keeps a chair value only where mavat is
// empty); the chair still reviews + saves. Slow — a real browser on the host.
async function pullFromMavat() {
const num = f.plan_number.trim();
if (!num) {
toast.error("הקלד מספר-תכנית למשיכה");
return;
}
try {
const r = await fetchMavat.mutateAsync(num);
setF((prev) => ({
plan_number: r.plan_number || prev.plan_number,
display_name: r.display_name || prev.display_name,
plan_type: r.plan_type || prev.plan_type,
gazette_date: r.gazette_date || prev.gazette_date,
yalkut_number: r.yalkut_number || prev.yalkut_number,
purpose: r.purpose || prev.purpose,
}));
setSourceUrl(r.source_url || "");
toast.success(
r.yalkut_number || r.gazette_date
? "נמשך ממנהל-התכנון — בדוק ואשר"
: "נמצא במנהל-התכנון אך ללא תוקף מפורסם — השלם ידנית",
);
} catch (e) {
const msg = (e as { body?: { detail?: string } })?.body?.detail;
toast.error(msg || "שגיאה במשיכה ממנהל-התכנון");
}
}
async function save() {
if (!f.plan_number.trim()) {
toast.error("חובה מספר-תכנית");
@@ -455,6 +489,28 @@ function PlanForm({
<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>
{/* pull validity from the official source (mavat) into the fields below */}
<div className="flex items-center gap-3 flex-wrap rounded-md border border-[#cdd9e8] bg-info-bg px-3 py-2.5">
<Button
size="sm"
className="bg-navy text-white hover:bg-navy-soft shrink-0"
disabled={fetchMavat.isPending}
onClick={pullFromMavat}
>
{fetchMavat.isPending ? (
<><Loader2 className="size-4 animate-spin" /> מושך</>
) : (
<><DownloadCloud className="size-4" /> משוך מ-מנהל-התכנון</>
)}
</Button>
<span className="text-[0.72rem] text-ink-soft leading-relaxed flex-1 min-w-[220px]">
{fetchMavat.isPending
? "מושך ממנהל-התכנון — עד כדקה (דפדפן חי)…"
: 'הקלד מספר-תכנית ולחץ — שם, תאריך-רשומות, מס\' ילקוט (י"פ), סוג וייעוד יימשכו מהמקור הרשמי. שדה שהמקור אינו חושף יישאר ריק — לא מומצא.'}
</span>
</div>
<div className="grid grid-cols-2 gap-3">
{field("plan_number", "מספר תכנית", 'מזהה בלבד (ללא המילה "תכנית")')}
{field("display_name", "שם תצוגה")}
@@ -468,6 +524,14 @@ function PlanForm({
תצוגה מקדימה כך ייכתב בבלוק ט:
</span>
<span className="text-[0.88rem] leading-relaxed">{previewCitation(f)}</span>
{sourceUrl && (
<a
href={sourceUrl} target="_blank" rel="noopener noreferrer"
className="mt-2 inline-flex items-center gap-1 text-[0.7rem] text-[#365070] hover:underline"
>
<ExternalLink className="size-3" /> מקור: מנהל-התכנון
</a>
)}
</div>
<div className="flex items-center gap-2 justify-end">
<Button variant="ghost" size="sm" className="me-auto text-ink-muted" onClick={onClose}>