import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { track } from "../../lib/analytics"; import { useNavigate, useSearchParams } from "react-router-dom"; import { ChevronRight, ChevronLeft, Check, ShieldCheck, AlertCircle, X } from "lucide-react"; import { toast } from "sonner"; import type { LineItem, Quote } from "../../lib/types"; import { api, ApiError, looksLikeUuid, nextId } from "../../lib/api"; import { useAddMaterial, useAddProject, useAddSupplier, useCreateQuote, useMaterials, useProjects, useQuotes, useStoreData, useSuppliers, } from "../../hooks"; import { topicByName } from "../../lib/topics"; import { useAppStore } from "../../store"; import { normalize } from "../../lib/normalize"; import { faNum } from "../../lib/formatters"; import { matchMaterial } from "../../lib/quoteUtils"; import { Header, PageLayout } from "../../components/layout/Header"; import { AppSwitcherMenu } from "../../components/AppSwitcherMenu"; import { Stepper } from "./Stepper"; import { Step1TopicSupplier } from "./Step1TopicSupplier"; import { Step2LineItems, type ItemDraft, type MaterialRec } from "./Step2LineItems"; import { Step3Termsheets, effectiveCovered, type TsCardDraft } from "./Step3Termsheets"; import { Step4Projects } from "./Step4Projects"; import { Step5Review } from "./Step5Review"; import { Button } from "../../components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog"; import { useAuthStore } from "../../store/auth"; const TOTAL_STEPS = 5; // ---------- صفحه خروج امن (Safe Exit Screen) ---------- function SafeExitScreen({ title, subtitle, }: { title?: string; subtitle?: string; }) { return (

{title || "پیش‌نویس استعلام ذخیره گردید"}

{subtitle || "اطلاعات با موفقیت ذخیره شد. اکنون می‌توانید این برگه را ببندید و برای ادامه کار به ربات بله بازگردید."}

(در صورت مسدود بودن بستن خودکار توسط مرورگر، لطفاً تب مرورگر را به صورت دستی ببندید)

); } // ---------- صفحه خطای توکن نامعتبر ---------- function TokenInvalidScreen({ message }: { message: string }) { return (

لینک نامعتبر یا منقضی شده

{message || "اعتبار این لینک بازبینی به پایان رسیده است. لطفاً از طریق ربات بله، مجدداً دکمه بازبینی را دریافت فرمایید."}

); } // ویزارد ۵ مرحله‌ای افزودن پیشنهاد جدید و بازبینی پیش‌نویس export function WizardPage({ isStandalone = false }: { isStandalone?: boolean }) { useStoreData(); const navigate = useNavigate(); const [params] = useSearchParams(); const token = params.get("token"); const isStandaloneMode = isStandalone || Boolean(token); const [exitInfo, setExitInfo] = useState<{ show: boolean; title?: string; subtitle?: string; }>({ show: false }); const [tokenError, setTokenError] = useState(null); // Setup History Trap when in standalone mode useEffect(() => { if (!isStandaloneMode) return; try { window.history.replaceState({ page: "review_root" }, "", window.location.href); window.history.pushState({ page: "review_active" }, "", window.location.href); } catch {} const onPop = () => { setExitInfo({ show: true, title: "بازبینی به پایان رسید", subtitle: "پیش‌نویس ذخیره گردید. اکنون می‌توانید این برگه را ببندید و به ربات بله بازگردید.", }); try { window.history.pushState(null, "", window.location.href); } catch {} }; window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); }, [isStandaloneMode]); const quotes = useQuotes(); const suppliers = useSuppliers(); const materials = useMaterials(); const projects = useProjects(); // سطح limited تدارکات: استعلام فقط برای پروژه‌های عضویت — بدون افزودن پروژه جدید const user = useAuthStore((s) => s.user); const isLimitedProcurement = user?.panels?.procurement === "limited" && user?.role !== "admin"; const memberProjectNames = useMemo(() => { const allowedIds = new Set([ ...(user?.member_project_ids ?? []), ...(user?.projects ?? []).map((p) => p.id), ...(user?.project_id ? [user.project_id] : []), ]); const matched = projects .filter((p) => allowedIds.has(p.id)) .map((p) => p.name); const directNames = (user?.projects ?? []).map((p) => p.name); if (user?.project_name) directNames.push(user.project_name); return [...new Set([...matched, ...directNames])]; }, [projects, user]); const topics = useAppStore((s) => s.topics); const createQuote = useCreateQuote(); const addSupplier = useAddSupplier(); const addMaterial = useAddMaterial(); const addProject = useAddProject(); // ---------- state ---------- const [currentStep, setCurrentStep] = useState(1); const [highestVisited, setHighestVisited] = useState(1); const [selectedTopic, setSelectedTopic] = useState(""); const [supplierName, setSupplierName] = useState(""); const [newSup, setNewSup] = useState({ location: "", phone: "", contact: "" }); const [isOfficial, setIsOfficial] = useState(false); const [supplierInvalid, setSupplierInvalid] = useState(false); const [items, setItems] = useState(() => [ { tempId: "it0", cat: "", brand: "", specs: [], qty: null, unit: "", price: null, total: null }, ]); const [cards, setCards] = useState(() => [ { key: "ts0", milestones: [{ when_iso: null, amount: null }] }, ]); const [coverage, setCoverage] = useState>({}); const [assignment, setAssignment] = useState>({}); const [quoteDate, setQuoteDate] = useState(() => { return new Date().toISOString(); }); const [expiryDate, setExpiryDate] = useState(null); const [notes, setNotes] = useState(""); const [proforma, setProforma] = useState(null); const [emptyFields, setEmptyFields] = useState<{ label: string; step: number }[]>([]); const [confirmOpen, setConfirmOpen] = useState(false); const [loadedDraftId, setLoadedDraftId] = useState(null); const draftFetchedRef = useRef(false); const lastRemoteUpdatedAtRef = useRef(null); const isSelfUpdatingRef = useRef(false); const isCommittedOrCancelledRef = useRef(false); const initialLoadedRef = useRef(false); // ---------- تابع اعمال داده‌های پیش‌نویس به فیلدهای ویزارد ---------- const applyDraftPayloadToForm = useCallback( (p: Record) => { if (!p) return; // ۱. تأمین‌کننده و وضعیت رسمی if (p.supplier_name !== undefined) { setSupplierName(p.supplier_name || ""); setSupplierInvalid(false); } if (p.is_official !== undefined) { setIsOfficial(Boolean(p.is_official)); } // استنتاج تاپیک: ابتدا از اطلاعات تأمین‌کننده، سپس متریال، سپس اولین تاپیک موجود let topicToSet = ""; if (p.supplier_name) { const sup = suppliers.find((s) => normalize(s.name) === normalize(p.supplier_name)); if (sup?.topic) topicToSet = sup.topic; } if (!topicToSet && Array.isArray(p.items) && p.items.length > 0) { for (const it of p.items) { if (it.material_name) { const mat = materials.find((m) => normalize(m.name) === normalize(it.material_name)); if (mat?.topic) { topicToSet = mat.topic; break; } } } } if (!topicToSet && topics.length > 0) { topicToSet = topics[0].name; } if (topicToSet) { setSelectedTopic(topicToSet); } // ۲. اقلام (Step 2) let newItems: ItemDraft[] = []; if (Array.isArray(p.items) && p.items.length > 0) { newItems = p.items.map((it: any, idx: number) => { const qty = it.quantity != null ? Number(it.quantity) : null; const price = it.unit_price != null ? Number(it.unit_price) : null; return { tempId: "it" + idx, cat: it.material_name || "", brand: it.brand_name || "", specs: Array.isArray(it.specs) ? it.specs : [], qty, unit: it.unit_name || "", price, total: qty != null && price != null ? qty * price : null, }; }); setItems(newItems); } // ۳. شرایط پرداخت / termsheet (Step 3) if (Array.isArray(p.termsheets) && p.termsheets.length > 0) { const newCards: TsCardDraft[] = p.termsheets.map((ts: any, idx: number) => ({ key: "ts" + idx, milestones: Array.isArray(ts.milestones) && ts.milestones.length > 0 ? ts.milestones.map((m: any) => ({ when_iso: m.due_date || m.when_iso || null, amount: m.amount != null ? Number(m.amount) : null, })) : [{ when_iso: null, amount: null }], })); setCards(newCards); const targetIds = (newItems.length > 0 ? newItems : items).map((i) => i.tempId); const newCov: Record = {}; newCards.forEach((c) => { newCov[c.key] = targetIds; }); setCoverage(newCov); } // ۴. پروژه (Step 4) const projNames: string[] = []; if (Array.isArray(p.project_names)) { projNames.push(...p.project_names.filter(Boolean)); } if (p.project_name && !projNames.includes(p.project_name)) { projNames.push(p.project_name); } if (projNames.length > 0) { const newAssignment: Record = {}; const targetItems = newItems.length > 0 ? newItems : items; targetItems.forEach((it) => { newAssignment[it.tempId] = projNames; }); setAssignment(newAssignment); } // ۵. یادداشت و تاریخ انقضا (Step 5) if (p.notes !== undefined) { setNotes(p.notes || ""); } if (p.expiry_date !== undefined) { setExpiryDate(p.expiry_date || null); } }, [suppliers, materials, topics, items], ); // ---------- prefill از لینک‌ها (?topic=&supplier=&project=) ---------- useEffect(() => { const t = params.get("topic"); if (t) { const topic = topicByName(t, topics); if (topic) { setSelectedTopic(topic.name); const sid = params.get("supplier"); if (sid) { const sup = suppliers.find((s) => s.id === sid); if (sup && normalize(sup.topic) === normalize(topic.name)) { setSupplierName(sup.name); setSupplierInvalid(false); } } } } const proj = params.get("project"); if (proj) { setAssignment((cur) => { const next = { ...cur }; for (const it of items) { if (!next[it.tempId]?.length) { next[it.tempId] = [proj]; } } return next; }); } }, [params, suppliers, topics, items]); // ---------- بارگذاری اولیه پیش‌نویس ربات بله (?token= یا ?draft=) ---------- useEffect(() => { const draftId = params.get("draft") || params.get("draft_id"); if ((!token && !draftId) || draftFetchedRef.current) return; draftFetchedRef.current = true; void (async () => { try { let draft: any = null; if (token) { const res = await api.drafts.verifyToken(token); draft = (res as any)?.data || res; } else if (draftId) { draft = await api.drafts.get(draftId); } if (!draft || !draft.payload) { if (token) setTokenError("پیش‌نویس معتبری یافت نشد."); return; } setLoadedDraftId(draft.id); lastRemoteUpdatedAtRef.current = draft.updated_at || null; applyDraftPayloadToForm(draft.payload); setHighestVisited(5); initialLoadedRef.current = true; toast.success("پیش‌نویس از ربات بله بارگذاری شد. لطفاً اطلاعات را بازبینی کنید.", { duration: 6000, }); } catch (err: any) { if (token) { setTokenError(err?.message || "لینک بازبینی نامعتبر یا منقضی شده است."); } else { toast.error(`خطا در فراخوانی پیش‌نویس از ربات: ${err instanceof Error ? err.message : "خطای ناشناخته"}`); } } })(); }, [params, token, applyDraftPayloadToForm]); // ---------- پولینگ دوره‌ای برای دریافت تغییرات یا ثبت از سمت ربات بله ---------- useEffect(() => { const draftId = params.get("draft") || params.get("draft_id"); if (!token && !draftId) return; const interval = setInterval(async () => { if (isCommittedOrCancelledRef.current || !initialLoadedRef.current) return; try { let draft: any = null; if (token) { const res = await api.drafts.verifyToken(token); draft = (res as any)?.data || res; } else if (draftId) { draft = await api.drafts.get(draftId); } if (!draft) return; if (draft.status === "committed") { isCommittedOrCancelledRef.current = true; if (isStandaloneMode) { setExitInfo({ show: true, title: "این پیشنهاد ثبت نهایی شد ✅", subtitle: "استعلام با موفقیت در سیستم ثبت گردید. می‌توانید این برگه را ببندید و به ربات بله بازگردید.", }); return; } toast.success("این پیشنهاد از طریق ربات بله با موفقیت ثبت نهایی شد.", { duration: 8000 }); navigate("/"); return; } if (draft.status === "cancelled") { isCommittedOrCancelledRef.current = true; if (isStandaloneMode) { setExitInfo({ show: true, title: "پیش‌نویس لغو شد", subtitle: "این پیش‌نویس از طریق ربات بله لغو گردید. می‌توانید این برگه را ببندید.", }); return; } toast.error("این پیش‌نویس از طریق ربات بله لغو شد.", { duration: 6000 }); navigate("/"); return; } if ( draft.updated_at && lastRemoteUpdatedAtRef.current && draft.updated_at > lastRemoteUpdatedAtRef.current && !isSelfUpdatingRef.current ) { lastRemoteUpdatedAtRef.current = draft.updated_at; applyDraftPayloadToForm(draft.payload); } } catch { /* خطای موقت شبکه در پولینگ نادیده گرفته می‌شود */ } }, 2000); return () => clearInterval(interval); }, [params, token, isStandaloneMode, navigate, applyDraftPayloadToForm]); // ---------- همگام‌سازی خودکار تغییرات ویزارد به پیش‌نویس ربات (Debounced Auto-Save) ---------- useEffect(() => { const draftId = params.get("draft") || params.get("draft_id"); if ((!token && !draftId) || isCommittedOrCancelledRef.current || !initialLoadedRef.current) return; const timer = setTimeout(async () => { isSelfUpdatingRef.current = true; try { const payloadToSave: Record = { supplier_name: supplierName.trim() || null, is_official: isOfficial, items: items.map((it) => ({ material_name: it.cat.trim() || null, brand_name: it.brand.trim() || null, specs: it.specs, quantity: it.qty, unit_name: it.unit.trim() || null, unit_price: it.price, })), termsheets: cards.map((c, i) => ({ label: "گزینه " + faNum(i + 1), milestones: (c.milestones || []).map((m) => ({ due_date: m.when_iso, amount: m.amount, })), })), project_names: [...new Set(Object.values(assignment).flat())], notes: notes.trim() || null, expiry_date: expiryDate || null, }; if (token) { await api.drafts.updateByToken(token, payloadToSave); const res = await api.drafts.verifyToken(token); const updated = (res as any)?.data || res; if (updated?.updated_at) { lastRemoteUpdatedAtRef.current = updated.updated_at; } } else if (draftId) { await api.drafts.update(draftId, payloadToSave); const updated = await api.drafts.get(draftId); if (updated?.updated_at) { lastRemoteUpdatedAtRef.current = updated.updated_at; } } } catch (err) { console.warn("Auto-save to draft failed:", err); } finally { setTimeout(() => { isSelfUpdatingRef.current = false; }, 600); } }, 800); return () => clearTimeout(timer); }, [supplierName, isOfficial, items, cards, assignment, notes, expiryDate, params, token]); // ---------- اعتبارسنجی قبل از ترک هر مرحله ---------- const completedSteps = useMemo(() => { const s = new Set(); if (selectedTopic && supplierName.trim()) s.add(1); if (items.length > 0 && items.some((it) => it.qty != null && it.qty !== 0 && it.price != null && it.price !== 0)) s.add(2); if ( cards.length > 0 && cards.some((card) => (card.milestones || []).some((m) => m.when_iso || (m.amount != null && m.amount !== 0))) ) { s.add(3); } if (items.length > 0 && items.every((it) => (assignment[it.tempId] || []).length > 0)) s.add(4); if (highestVisited >= 5) s.add(5); return s; }, [selectedTopic, supplierName, items, cards, coverage, assignment, highestVisited]); const goToStep = (n: number) => { setCurrentStep(n); setHighestVisited((h) => Math.max(h, n)); track("wizard_step", { step: n, direction: n > currentStep ? "forward" : "back" }); window.scrollTo({ top: 0, behavior: "smooth" }); }; const next = () => { goToStep(currentStep + 1); }; const rememberMaterial = (_rec: MaterialRec) => {}; // ---------- پروژه‌ها ---------- const projectNames = useMemo(() => { if (isLimitedProcurement) { return memberProjectNames; } const names = new Set(projects.map((p) => p.name)); quotes.forEach((q) => { if (q.project) names.add(q.project); (q.projects || []).forEach((n) => names.add(n)); }); return [...names]; }, [projects, quotes, isLimitedProcurement, memberProjectNames]); // سطح محدود: در صورت داشتن یک پروژه، خودکار اقلام به آن پروژه تخصیص می‌یابند useEffect(() => { if (isLimitedProcurement && memberProjectNames.length === 1) { const sole = memberProjectNames[0]; setAssignment((cur) => { let changed = false; const next = { ...cur }; for (const it of items) { if (!next[it.tempId]?.length) { next[it.tempId] = [sole]; changed = true; } } return changed ? next : cur; }); } }, [isLimitedProcurement, memberProjectNames, items]); const assign = (tempId: string, proj: string) => { setAssignment((a) => { const cur = a[tempId] || []; if (cur.includes(proj)) return a; return { ...a, [tempId]: [...cur, proj] }; }); }; const unassign = (tempId: string, proj: string) => { setAssignment((a) => { const cur = (a[tempId] || []).filter((p) => p !== proj); const next = { ...a }; if (cur.length) next[tempId] = cur; else delete next[tempId]; return next; }); }; // ---------- خلاصه مرحله ۵ ---------- const review = useMemo(() => { let grandTotal = 0; items.forEach((it) => { const t = it.total; if (t) grandTotal += t; }); const projSet = new Set(); items.forEach((it) => (assignment[it.tempId] || []).forEach((p) => projSet.add(p))); const coveredTempIds = new Set(); cards.forEach((card) => effectiveCovered(cards, coverage, items, card.key).forEach((tid) => coveredTempIds.add(tid)), ); const orphanCount = items.filter((it) => !coveredTempIds.has(it.tempId)).length; return { supplierName: supplierName.trim() || "—", topicName: selectedTopic || "—", projects: [...projSet], itemCount: items.length, tsCount: cards.length, grandTotal, isOfficial, orphanCount, }; }, [items, assignment, cards, coverage, supplierName, selectedTopic, isOfficial]); // ---------- ذخیره ---------- const doSave = async () => { const resolveErrorTarget = (err: unknown): { step: number; field?: string } => { let field = ""; let msg = ""; if (err instanceof ApiError) { field = err.field || ""; msg = err.message || ""; } else if (err instanceof Error) { msg = err.message || ""; if ("field" in err && typeof (err as any).field === "string") { field = (err as any).field; } } const combined = `${field} ${msg}`.toLowerCase(); if (/supplier|تأمین|تامین/i.test(combined)) return { step: 1, field: "supplier_id" }; if (/line_item|material|canonical_unit|unit_id|quantity|متریال|واحد|قلم|تعداد/i.test(combined)) { return { step: 2, field: field || "line_items" }; } if (/termsheet|item_termsheet|schedule_step|due_date|milestone|ترم|پرداخت|پوشش|سررسید/i.test(combined)) { return { step: 3, field: field || "termsheets" }; } if (/project|پروژه/i.test(combined)) return { step: 4, field: "project" }; if (/expiry_date|تاریخ انقضا/i.test(combined)) return { step: 5, field: "expiry_date" }; if (/quote_date|تاریخ پیشنهاد/i.test(combined)) return { step: 5, field: "quote_date" }; if (/proforma|پیش‌فاکتور/i.test(combined)) return { step: 5, field: "proforma" }; if (/notes|یادداشت/i.test(combined)) return { step: 5, field: "notes" }; return { step: 5, field }; }; const sectionError = (stepOrTarget: number | { step: number; field?: string }, title: string, err: unknown) => { const target = typeof stepOrTarget === "number" ? { step: stepOrTarget } : stepOrTarget; const msg = err instanceof Error ? err.message : "خطای ناشناخته"; toast.error(`${title}: ${msg}`, { duration: 8000, action: { label: "رفع خطا", onClick: () => { goToStep(target.step); setTimeout(() => { const targetAttr = target.field ? `[data-field="${target.field}"]` : ""; const el = (targetAttr ? document.querySelector(targetAttr) : null) || document.querySelector(`[data-step="${target.step}"]`); if (el instanceof HTMLElement) { el.scrollIntoView({ behavior: "smooth", block: "center" }); const input = el.querySelector("input, textarea, select") as HTMLElement | null; if (input) input.focus(); else el.focus?.(); el.classList.add("ring-2", "ring-red-500", "rounded-xl"); setTimeout(() => el.classList.remove("ring-2", "ring-red-500", "rounded-xl"), 3500); } }, 150); }, }, }); }; const name = supplierName.trim(); let supplierId = ""; if (name) { const exist = suppliers.find((s) => normalize(s.name) === normalize(name)); if (exist) { supplierId = exist.id; } else { try { const created = await addSupplier.mutateAsync({ id: nextId("s", suppliers), topic: selectedTopic, categories: [], name, location: newSup.location.trim(), phone: newSup.phone.trim(), contact: newSup.contact.trim(), }); supplierId = created.id; } catch (err) { sectionError(1, "خطا در بخش تأمین‌کننده", err); return; } } } const collected: { li: LineItem; tempId: string }[] = []; for (const it of items) { const cat = it.cat.trim(); const brand = it.brand.trim(); const specs = it.specs; const unit = it.unit.trim(); const qty = it.qty; const price = it.price; const total = it.total; if (!cat && !brand && qty == null && price == null) continue; const wantName = (cat || brand || specs.join(" ") || "").trim() || "کالای جدید"; let material = materials.find((m) => looksLikeUuid(m.id) && normalize(m.name) === normalize(wantName)); if (!material) { const matched = matchMaterial( materials.filter((m) => looksLikeUuid(m.id)), cat, brand, specs, ); if (matched) material = matched; } let materialId: string; if (material && looksLikeUuid(material.id)) { materialId = material.id; } else { try { const created = await addMaterial.mutateAsync({ id: nextId("m", materials), name: wantName, topic: selectedTopic, brand, specs, base_unit: unit || "عدد", }); materialId = created.id; } catch (err) { sectionError(2, "خطا در بخش اقلام (متریال)", err); return; } } collected.push({ li: { id: "li-" + (collected.length + 1), material_id: materialId, quantity: qty, unit, unit_price: price, total_price: total, termsheet_ids: [], accepted_ts_id: null, rejected: false, arrived_date: null, }, tempId: it.tempId, }); } const lineItems = collected.map((c) => c.li); const termsheets = cards .map((card, i) => ({ id: "new-ts-" + (i + 1), label: "گزینه " + faNum(i + 1), milestones: card.milestones.filter((m) => m.when_iso || m.amount != null), status: "offered" as const, })) .filter((t) => t.milestones.length); collected.forEach(({ li, tempId }) => { const ids = cards .map((card, i) => effectiveCovered(cards, coverage, items, card.key).includes(tempId) ? "new-ts-" + (i + 1) : "", ) .filter(Boolean); const valid = ids.filter((tid) => termsheets.some((t) => t.id === tid)); li.termsheet_ids = valid.length ? valid : termsheets.length ? [termsheets[0].id] : []; }); const projSet = new Set(); collected.forEach(({ li, tempId }) => { const list = assignment[tempId] || []; li.projects = list.map((p) => ({ id: "", name: p })); list.forEach((p) => projSet.add(p)); }); const projectsList = [...projSet]; const quoteIso = quoteDate || new Date().toISOString(); const quote: Quote = { id: nextId("q", quotes), supplier_id: supplierId, project: projectsList[0] || "", projects: projectsList, quote_date: quoteIso, expiry_date: expiryDate, accepted_date: null, arrived_date: null, status: "offered", is_official: isOfficial, proformas: [], invoices: [], events: [{ type: "offered", occurred_at: quoteIso }], notes: notes.trim(), line_items: lineItems, termsheets, }; try { const created = await createQuote.mutateAsync(quote); const token = params.get("token"); const draftId = params.get("draft") || params.get("draft_id"); if (token) { isCommittedOrCancelledRef.current = true; try { await api.drafts.commitByToken(token, created.id); } catch (err) { console.warn("Could not commit draft via token:", err); } } else if (draftId) { isCommittedOrCancelledRef.current = true; try { await api.drafts.commit(draftId, created.id); } catch (err) { console.warn("Could not commit draft:", err); } } if (proforma) { try { await api.files.upload(created.id, "proforma", proforma); } catch (err) { toast.error(`خطا در آپلود پیش‌فاکتور: ${err instanceof Error ? err.message : "خطای ناشناخته"}`, { action: { label: "رفع خطا", onClick: () => goToStep(5) }, }); } } toast.success("پیشنهاد ذخیره شد و در ربات بله تأیید گردید."); track("quote_submitted", { quote_id: created.id, items: lineItems.length }); if (isStandaloneMode) { setExitInfo({ show: true, title: "پیشنهاد با موفقیت در سیستم ثبت گردید ✅", subtitle: `شناسه استعلام: #${created.id.slice(0, 8)} — تغییرات شما با موفقیت اعمال شد. اکنون می‌توانید این برگه را ببندید و به ربات بله بازگردید.`, }); return; } navigate("/"); } catch (e) { const target = resolveErrorTarget(e); sectionError(target, "خطا در ثبت پیشنهاد", e); } }; const handleCancel = async () => { const token = params.get("token"); const draftId = params.get("draft") || params.get("draft_id"); isCommittedOrCancelledRef.current = true; if (token) { try { await api.drafts.cancelByToken(token); } catch (err) { console.warn("Could not cancel draft via token:", err); } } else if (draftId) { try { await api.drafts.cancel(draftId); } catch (err) { console.warn("Could not cancel draft:", err); } } if (isStandaloneMode) { setExitInfo({ show: true, title: "پیش‌نویس لغو شد", subtitle: "این پیش‌نویس استعلام لغو گردید. می‌توانید این برگه را ببندید و به ربات بله بازگردید.", }); return; } navigate("/"); }; const save = () => { const incomplete: { label: string; step: number }[] = []; if (!completedSteps.has(1)) incomplete.push({ label: "تأمین‌کننده", step: 1 }); if (!completedSteps.has(2)) incomplete.push({ label: "اقلام", step: 2 }); if (!completedSteps.has(3)) incomplete.push({ label: "شرایط پرداخت", step: 3 }); if (!completedSteps.has(4)) incomplete.push({ label: "پروژه", step: 4 }); if (incomplete.length > 0) { setEmptyFields(incomplete); setConfirmOpen(true); return; } void doSave(); }; if (exitInfo.show) { return ; } if (tokenError) { return ; } return (
{isStandaloneMode ? (

بازبینی و تکمیل پیش‌نویس استعلام

ربات بله

محیط امن بازبینی — اطلاعات با ربات بله همگام‌سازی می‌شود

) : (
} /> )} {loadedDraftId && (
ربات بله اطلاعات پیش‌نویس استعلام با هوش مصنوعی بارگذاری شد. لطفاً اقلام و شرایط را بازبینی فرمایید.
#{loadedDraftId.slice(0, 8)}
)} {/* نوار مراحل استپر */} {/* محتوای مراحل با پدینگ ریسپانسیو و بهینه برای موبایل */} {currentStep === 1 && (
{ setSelectedTopic(t); setSupplierName(""); setNewSup({ location: "", phone: "", contact: "" }); setIsOfficial(false); setSupplierInvalid(false); }} supplierName={supplierName} onSupplierName={(v) => { setSupplierName(v); setSupplierInvalid(false); }} newSup={newSup} onNewSup={(p) => setNewSup((s) => ({ ...s, ...p }))} isOfficial={isOfficial} onOfficial={setIsOfficial} invalid={supplierInvalid} />
)} {currentStep === 2 && (
)} {currentStep === 3 && (
setCoverage((c) => ({ ...c, [key]: ids }))} />
)} {currentStep === 4 && (
{ void addProject.mutateAsync({ id: nextId("p", projects), name }); }} />
)} {currentStep === 5 && (
)} {/* پاورقی کنترل مراحل */}
{currentStep > 1 && ( )}
{/* دکمه‌های اقدام (انصراف + بعدی / ذخیره) */}
{currentStep < TOTAL_STEPS && ( )} {currentStep === TOTAL_STEPS && ( )}
{/* دیالوگ فیلدهای خالی */} فیلدهای خالی
هنوز تعدادی از فیلدها خالی هستند. آیا مطمئنید که می‌خواهید ادامه دهید؟
{emptyFields.map((f) => ( ))}
); }