feat(plans): כפתור "משוך מ-מנהל-התכנון" בטופס-התכנית (Phase C טריגר 1) #294

Merged
chaim merged 1 commits from worktree-plan-fetch-button into main 2026-06-17 11:36:15 +00:00
2 changed files with 91 additions and 1 deletions
Showing only changes of commit 2b1fb18dfd - Show all commits

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}>

View File

@@ -81,6 +81,32 @@ export function usePlanDuplicates(planId: string, enabled = true) {
});
}
/** The candidate mavat returns for the form (INV-AH: every value carries source_url). */
export type PlanFetchResult = {
plan_number: string;
display_name: string;
plan_type: string;
purpose: string;
gazette_date: string; // ISO YYYY-MM-DD, "" if mavat doesn't expose it
yalkut_number: string;
yalkut_page: string;
source_url: string; // the mavat plan page
};
/**
* Pull a plan's identity + validity from mavat (מנהל התכנון) to prefill the form.
* Slow — drives a real browser on the host bridge (~½1 min); surface a spinner.
*/
export function useFetchPlan() {
return useMutation({
mutationFn: (planNumber: string) =>
apiRequest<PlanFetchResult>(`/api/plans/fetch`, {
method: "POST",
body: { plan_number: planNumber },
}),
});
}
export type PlanUpsert = {
plan_number: string;
display_name?: string;