feat(plans): כפתור "משוך מ-מנהל-התכנון" בטופס-התכנית (Phase C טריגר 1) #294
@@ -3,6 +3,7 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Check, X, Edit2, AlertTriangle, Plus, GitMerge, Save, Search, Undo2, BadgeCheck,
|
Check, X, Edit2, AlertTriangle, Plus, GitMerge, Save, Search, Undo2, BadgeCheck,
|
||||||
|
DownloadCloud, Loader2, ExternalLink,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -13,7 +14,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
usePlansAll, usePlanDuplicates, useUpsertPlan, useUpdatePlan,
|
usePlansAll, usePlanDuplicates, useUpsertPlan, useUpdatePlan,
|
||||||
useReviewPlan, useMergePlans, type Plan, type PlanEdit,
|
useReviewPlan, useMergePlans, useFetchPlan, type Plan, type PlanEdit,
|
||||||
} from "@/lib/api/plans";
|
} from "@/lib/api/plans";
|
||||||
|
|
||||||
/* Strip bidi marks (mirror of the halacha panel's cleanCitation). */
|
/* Strip bidi marks (mirror of the halacha panel's cleanCitation). */
|
||||||
@@ -401,14 +402,47 @@ function PlanForm({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [f, setF] = useState<EditForm>(initial);
|
const [f, setF] = useState<EditForm>(initial);
|
||||||
|
const [sourceUrl, setSourceUrl] = useState("");
|
||||||
const upsert = useUpsertPlan();
|
const upsert = useUpsertPlan();
|
||||||
const update = useUpdatePlan();
|
const update = useUpdatePlan();
|
||||||
|
const fetchMavat = useFetchPlan();
|
||||||
const busy = upsert.isPending || update.isPending;
|
const busy = upsert.isPending || update.isPending;
|
||||||
|
|
||||||
function set<K extends keyof EditForm>(k: K, v: string) {
|
function set<K extends keyof EditForm>(k: K, v: string) {
|
||||||
setF((prev) => ({ ...prev, [k]: v }));
|
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() {
|
async function save() {
|
||||||
if (!f.plan_number.trim()) {
|
if (!f.plan_number.trim()) {
|
||||||
toast.error("חובה מספר-תכנית");
|
toast.error("חובה מספר-תכנית");
|
||||||
@@ -455,6 +489,28 @@ function PlanForm({
|
|||||||
<h3 className="text-navy text-sm font-bold m-0">{title}</h3>
|
<h3 className="text-navy text-sm font-bold m-0">{title}</h3>
|
||||||
<p className="text-ink-muted text-xs mt-0.5">{subtitle}</p>
|
<p className="text-ink-muted text-xs mt-0.5">{subtitle}</p>
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{field("plan_number", "מספר תכנית", 'מזהה בלבד (ללא המילה "תכנית")')}
|
{field("plan_number", "מספר תכנית", 'מזהה בלבד (ללא המילה "תכנית")')}
|
||||||
{field("display_name", "שם תצוגה")}
|
{field("display_name", "שם תצוגה")}
|
||||||
@@ -468,6 +524,14 @@ function PlanForm({
|
|||||||
תצוגה מקדימה — כך ייכתב בבלוק ט:
|
תצוגה מקדימה — כך ייכתב בבלוק ט:
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[0.88rem] leading-relaxed">{previewCitation(f)}</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>
|
||||||
<div className="flex items-center gap-2 justify-end">
|
<div className="flex items-center gap-2 justify-end">
|
||||||
<Button variant="ghost" size="sm" className="me-auto text-ink-muted" onClick={onClose}>
|
<Button variant="ghost" size="sm" className="me-auto text-ink-muted" onClick={onClose}>
|
||||||
|
|||||||
@@ -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 = {
|
export type PlanUpsert = {
|
||||||
plan_number: string;
|
plan_number: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user