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