"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 (
{[0, 1, 2].map((i) => )}
); } if (isError) { return

שגיאה בטעינת תור-התכניות.

; } const plans = data?.items ?? []; return (

מרשם-התכניות: זהות + תוקף קנוניים של כל תב"ע, בשימוש חוזר בין תיקים. רק תכנית מאושרת מצוטטת בבלוק ט; התוקף נכתב בנוסח אחיד{" "} דטרמיניסטי — תאריך-הרשומות ומס' הילקוט לעולם אינם מומצאים.

{adding && ( setAdding(false)} /> )} {plans.length === 0 ? (
אין תכניות הממתינות לאישור.
) : ( plans.map((p) => ) )}
); } // ─── 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 ( setEditing(false)} /> ); } return (
תכנית {plan.display_name || plan.plan_number} {plan.plan_type && ( {plan.plan_type} )} {plan.source_case_number && ( מקור: {plan.source_case_number} )}
{duplicates.length > 0 && (
כפילות אפשרית — ייתכן שאותה תכנית כבר במרשם בצורה אחרת. מזג כדי לא לפצל תכנית אחת לשתי רשומות.
{duplicates.map((d) => (
{d.display_name || d.plan_number} {d.match_reason && ( ({d.match_reason}) )}
))}
)}
משפט-הציטוט הקנוני (כפי שייכתב בבלוק ט)
{clean(plan.citation_formatted)}
תאריך רשומות: {fmtDate(plan.gazette_date)}
ילקוט פרסומים: {plan.yalkut_number || "—"}
{plan.plan_type &&
סוג: {plan.plan_type}
}
{noDate && (
⚠ חסר תאריך-תוקף — תצוטט ללא תוקף עד השלמה.
)}
); } // ─── 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(initial); const upsert = useUpsertPlan(); const update = useUpdatePlan(); const busy = upsert.isPending || update.isPending; function set(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, ) => (
set(k, e.target.value)} /> {hint &&

{hint}

}
); return (

{title}

{subtitle}

{field("plan_number", "מספר תכנית", 'מזהה בלבד (ללא המילה "תכנית")')} {field("display_name", "שם תצוגה")} {field("gazette_date", "תאריך פרסום ברשומות", "YYYY-MM-DD")} {field("yalkut_number", 'מס\' ילקוט פרסומים (י"פ)')} {field("plan_type", "סוג תכנית", "ארצית / מחוזית / מקומית / מפורטת / כוללנית")} {field("purpose", "ייעוד (משפט אחד)", undefined, true)}
תצוגה מקדימה — כך ייכתב בבלוק ט: {previewCitation(f)}
); }