import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { toast } from "sonner"; import { Header, PageLayout } from "../../components/layout/Header"; import { Combobox } from "../../components/Combobox"; import { MultiSelectCombobox } from "../../components/MultiSelectCombobox"; import { useAuthStore } from "../../store/auth"; import { inventorySubheader } from "./panelMenu"; import { api, apiV26 } from "../../lib/api"; import { notifyDataChanged } from "../../lib/crossTab"; import { useInventoryData, unitName, whName } from "./useInventoryData"; import { JalaliDatePicker } from "../../components/JalaliDatePicker"; import { uniqueUnits } from "../../lib/units"; import type { InventoryTransaction, TransactionType, Project, ProjectTransfer } from "../../lib/types"; import { faNum, fmtDate } from "../../lib/formatters"; import { gregorianToJalali, MONTHS, toFa } from "../../lib/jalali"; import { cn } from "../../lib/utils"; type FormType = TransactionType | "TRANSFER" | "PROJECT_TRANSFER"; const FORM_TYPES: { t: FormType; label: string; dir: string; cls: "in" | "out" | "mv" | "cnt"; color: string; desc: string }[] = [ { t: "PURCHASE", label: "موجودی اولیه", dir: "افزایش", cls: "in", color: "#10b981", desc: "ثبت موجودی اولیه — متریال/برند/واحد را از فهرست انتخاب کنید؛ اگر نامی در فهرست نبود، همان نام تایپ‌شده هنگام ثبت به‌صورت خودکار در کاتالوگ سراسری ساخته می‌شود. ثبت حداقل یک مشخصه فنی الزامی است." }, { t: "WITHDRAWAL", label: "برداشت", dir: "کاهش", cls: "out", color: "#e11d48", desc: "انبار را انتخاب و نام متریال را تایپ کنید؛ همه مدل‌ها (برند و مشخصه متفاوت) نمایش داده می‌شوند. برداشت‌ها در «کنترل روزانه» امروز دیده می‌شوند." }, { t: "PROJECT_TRANSFER", label: "انتقال بین کارگاهی", dir: "کاهش", cls: "out", color: "#e11d48", desc: "انتقال متریال از انبار این پروژه به یک پروژه دیگر — پس از ثبت، موجودی در انبار مبدأ کسر شده و کالا به عنوان بار در راه برای پروژه مقصد ارسال می‌شود تا مدیر پروژه مقصد آن را تحویل بگیرد." }, { t: "RETURN", label: "برگشت", dir: "افزایش", cls: "in", color: "#10b981", desc: "برگشت از مصالحی که امروز برداشت شده‌اند — نام متریال را تایپ کنید و مقدار برگشتی هر ردیف را بزنید." }, { t: "TRANSFER", label: "انتقال", dir: "جابه‌جایی", cls: "mv", color: "#2563eb", desc: "جابه‌جایی بین انبارهای پروژه — از انبار مبدأ، مقداری از هر مدل را به انبار مقصد منتقل کنید." }, { t: "DAMAGE", label: "ضایعات", dir: "کاهش", cls: "out", color: "#e11d48", desc: "ثبت ضایعات خرید (مثلاً لوله‌های سوراخ) — انبار و متریال را انتخاب و تعداد ضایعات هر مدل را وارد کنید." }, { t: "WASTE", label: "اتلاف", dir: "کاهش", cls: "out", color: "#e11d48", desc: "اتلاف بعد از خرید (خرابی در انبار یا هدررفت در کار) — انبار و متریال را انتخاب کنید." }, { t: "LOSS", label: "مفقودی", dir: "کاهش", cls: "out", color: "#e11d48", desc: "ثبت مفقودی — انبار و متریال را انتخاب و تعداد مفقودی هر مدل را وارد کنید." }, { t: "FOUND", label: "پیدا شده", dir: "افزایش", cls: "in", color: "#10b981", desc: "ثبت پیدا شدن کالاهای مفقود — مفقودی‌های همان انبار و متریال (یا «همه مفقودی‌ها») نمایش داده می‌شود." }, { t: "STOCK_COUNT", label: "شمارش موجودی", dir: "اصلاح", cls: "cnt", color: "#2563eb", desc: "شمارش فیزیکی — مقدار شمارش‌شده هر رکورد را وارد کنید تا موجودی اصلاح شود و اختلاف ثبت گردد." }, ]; interface RowDraft { key: string; qty: string; note: string; } function todayKey(): string { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function gregKey(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function jalDateLabel(d: Date): string { const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate()); return `${toFa(jd)} ${MONTHS[jm - 1]} ${toFa(jy)}`; } export function ChangeInventoryPage() { const user = useAuthStore((s) => s.user); const data = useInventoryData(); const [params] = useSearchParams(); const [type, setType] = useState("PURCHASE"); const [whId, setWhId] = useState(""); const [toProjectId, setToProjectId] = useState(""); const [allProjects, setAllProjects] = useState([]); const [transfers, setTransfers] = useState([]); const [loadingTransfers, setLoadingTransfers] = useState(false); // Edit modal state const [editingTransfer, setEditingTransfer] = useState(null); const [editQty, setEditQty] = useState(""); const [editToProjectId, setEditToProjectId] = useState(""); const [editNote, setEditNote] = useState(""); const [editBusy, setEditBusy] = useState(false); // Delete modal state const [deleteConfirmTransfer, setDeleteConfirmTransfer] = useState(null); const [deleteBusy, setDeleteBusy] = useState(false); const [fromId, setFromId] = useState(""); const [toId, setToId] = useState(""); const [matTxt, setMatTxt] = useState(""); const [brandTxt, setBrandTxt] = useState(""); const [unitTxt, setUnitTxt] = useState(""); const [qty, setQty] = useState(""); const [dateInput, setDateInput] = useState(() => new Date().toISOString()); const [specsChips, setSpecsChips] = useState([]); const [note, setNote] = useState(""); const [rows, setRows] = useState([]); const [busy, setBusy] = useState(false); useEffect(() => { const t = params.get("type"); const w = params.get("wh"); const m = params.get("mat"); if (t && FORM_TYPES.some((f) => f.t === t)) { setType(t as TransactionType); if (w) setWhId(w); if (m) setMatTxt(m); } }, [params]); const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; const isPM = user?.role === "pm"; const warehouses = data.warehouses; const whOpts = useMemo( () => (isAdmin ? warehouses : warehouses.filter((w) => String(w.project_id) === String(user?.project_id || ""))), [warehouses, isAdmin, user?.project_id], ); const formRecords = useMemo(() => { const txt = matTxt.trim(); const wid = type === "TRANSFER" ? fromId : whId; return data.inventory.filter((r) => { if (r.warehouse?.id !== wid) return false; if (txt && !r.material.name.includes(txt)) return false; return true; }); }, [data.inventory, whId, fromId, type, matTxt]); const matByName = (name: string) => data.lookup.materials.find((m) => m.name === name) || null; const brandIdByName = (name: string) => data.lookup.brands.find((b) => b.name === name)?.id || null; const unitIdByName = (name: string) => data.lookup.units.find((u) => (u.name_fa || u.code) === name)?.id || null; const parseNum = (s: string) => { const v = parseFloat(String(s).replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))).replace(/[،,]/g, "")); return isFinite(v) ? v : NaN; }; const toServerIso = () => { try { const d = new Date(dateInput); if (!Number.isNaN(d.getTime())) return d.toISOString(); } catch { /* نادیده */ } return new Date().toISOString(); }; const specsFromChips = () => { const out: Record = {}; specsChips.forEach((c) => { const k = c.trim(); if (k) out[k] = ""; }); return out; }; const specsComplete = specsChips.some((c) => c.trim()); const todayWithdrawals = useMemo(() => { const tk = todayKey(); const txt = matTxt.trim(); return data.transactions .filter((t) => { if (t.transaction_type !== "WITHDRAWAL" || t.warehouse_id !== whId) return false; if (gregKey(new Date(t.transaction_date)) !== tk) return false; if (txt && !(t.material?.name || "").includes(txt)) return false; return true; }) .sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime()); }, [data.transactions, whId, matTxt]); const lossTxs = useMemo(() => { const txt = matTxt.trim(); return data.transactions .filter((t) => { if (t.transaction_type !== "LOSS" || t.warehouse_id !== whId) return false; if (txt && !(t.material?.name || "").includes(txt)) return false; return true; }) .sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime()); }, [data.transactions, whId, matTxt]); const foundRemaining = useMemo(() => { const foundByKey = new Map(); data.transactions .filter((x) => x.transaction_type === "FOUND") .forEach((x) => { const k = lossKey(x); foundByKey.set(k, (foundByKey.get(k) || 0) + Math.abs(x.quantity_number || 0)); }); return lossTxs.map((t) => { const loss = Math.abs(t.quantity_number || 0); const found = foundByKey.get(lossKey(t)) || 0; return Math.max(0, Math.round((loss - found) * 10000) / 10000); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [lossTxs, data.transactions, whId, matTxt]); const lossKey = (t: InventoryTransaction) => [t.warehouse_id, t.material?.id || "", t.brand?.id || "", JSON.stringify(t.specs || {})].join("|"); const pendingLosses = useMemo( () => lossTxs .map((tx, i) => ({ tx, remaining: foundRemaining[i] ?? 0 })) .filter((x) => x.remaining > 0), [lossTxs, foundRemaining], ); const postOne = async (op: Record) => { await apiV26.inventory.createTransaction(op as never); }; const postSeq = async (ops: Record[]) => { setBusy(true); try { for (const op of ops) await postOne(op); toast.success("ثبت شد ✓"); notifyDataChanged("inventory"); setRows([]); setMatTxt(""); setBrandTxt(""); setUnitTxt(""); setQty(""); setSpecsChips([]); setNote(""); await data.refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در ثبت"); } finally { setBusy(false); } }; const loadTransfers = useCallback(async () => { if (!user?.project_id) return; setLoadingTransfers(true); try { const list = await apiV26.projectTransfers.list(user.project_id, "outgoing"); setTransfers(Array.isArray(list) ? list : []); } catch (err) { console.error("Failed to load project transfers:", err); setTransfers([]); } finally { setLoadingTransfers(false); } }, [user?.project_id]); useEffect(() => { api.projects .list() .then((res) => setAllProjects(Array.isArray(res) ? res : [])) .catch(() => setAllProjects([])); }, []); useEffect(() => { if (type === "PROJECT_TRANSFER") { void loadTransfers(); } }, [type, loadTransfers]); const whStock = useMemo(() => { if (!whId) return []; return (data.inventory || []).filter((r) => r.warehouse?.id === whId && r.quantity_number > 0); }, [data.inventory, whId]); const handleSaveEdit = async () => { if (!editingTransfer) return; const n = parseNum(editQty); if (!(n > 0)) return toast.error("تعداد باید بزرگ‌تر از صفر باشد"); if (!editToProjectId) return toast.error("پروژه مقصد را انتخاب کنید"); setEditBusy(true); try { await apiV26.projectTransfers.update(editingTransfer.id, { quantity: n, to_project_id: editToProjectId, note: editNote, }, user?.project_id); toast.success("انتقال با موفقیت ویرایش شد ✓"); notifyDataChanged("inventory"); setEditingTransfer(null); await data.refresh(); await loadTransfers(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در ویرایش انتقال"); } finally { setEditBusy(false); } }; const handleConfirmDelete = async () => { if (!deleteConfirmTransfer) return; setDeleteBusy(true); try { await apiV26.projectTransfers.delete(deleteConfirmTransfer.id, user?.project_id); toast.success("انتقال لغو شد و موجودی به انبار مبدأ بازگردانده شد ✓"); notifyDataChanged("inventory"); setDeleteConfirmTransfer(null); await data.refresh(); await loadTransfers(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در لغو انتقال"); } finally { setDeleteBusy(false); } }; const submitPurchase = async () => { const matName = matTxt.trim(); const brandName = brandTxt.trim(); const unitName = unitTxt.trim(); const n = parseNum(qty); if (!whId) return toast.error("انبار را انتخاب کنید"); if (!matName) return toast.error("نام متریال را وارد کنید"); if (!brandName) return toast.error("نام برند را وارد کنید"); if (!unitName) return toast.error("نام واحد را وارد کنید"); if (!(n > 0)) return toast.error("تعداد باید بزرگ‌تر از صفر باشد"); if (!specsComplete) return toast.error("حداقل یک مشخصه فنی ثبت کنید"); try { let uid = unitIdByName(unitName); if (!uid) { const created = await apiV26.catalog.createUnit(unitName); uid = created.id; } let bid = brandIdByName(brandName); if (!bid) { const created = await apiV26.catalog.createBrand(brandName); bid = created.id; } let m = matByName(matName); if (!m) { const created = await apiV26.catalog.createMaterial({ name: matName, canonical_unit_id: uid, brand_ids: bid ? [bid] : [] }); m = { id: created.id, name: created.name, canonical_unit: null }; } const op: Record = { transaction_type: "PURCHASE", warehouse_id: whId, material_id: m.id, brand_id: bid, quantity: n, unit_id: uid, specs: specsFromChips(), note, transaction_date: toServerIso(), }; await postSeq([op]); } catch (err) { setBusy(false); toast.error(err instanceof Error ? err.message : "خطا در ثبت موجودی اولیه"); } }; const submitProjectTransfer = async () => { const matName = matTxt.trim(); const brandName = brandTxt.trim(); const unitName = unitTxt.trim(); const n = parseNum(qty); if (!whId) return toast.error("انبار مبدأ را انتخاب کنید"); if (!toProjectId) return toast.error("پروژه مقصد را انتخاب کنید"); if (!matName) return toast.error("نام متریال را وارد کنید"); if (!unitName) return toast.error("نام واحد را وارد کنید"); if (!(n > 0)) return toast.error("تعداد باید بزرگ‌تر از صفر باشد"); setBusy(true); try { // Find matching inventory in the selected source warehouse const invInWh = (data.inventory || []).filter( (r) => r.warehouse?.id === whId && r.material.name.trim() === matName ); const matchedInv = invInWh.find( (r) => (brandName ? r.brand?.name === brandName : !r.brand) ) || invInWh[0]; let uid = matchedInv?.unit?.id ?? unitIdByName(unitName); if (!uid) { const created = await apiV26.catalog.createUnit(unitName); uid = created.id; } let bid: string | null = matchedInv?.brand?.id ?? (brandName ? brandIdByName(brandName) : null); if (brandName && !bid) { const created = await apiV26.catalog.createBrand(brandName); bid = created.id; } let mid = matchedInv?.material.id; if (!mid) { let m = matByName(matName); if (!m) { const created = await apiV26.catalog.createMaterial({ name: matName, canonical_unit_id: uid, brand_ids: bid ? [bid] : [] }); mid = created.id; } else { mid = m.id; } } const finalSpecs = matchedInv ? (matchedInv.specs || {}) : specsFromChips(); await apiV26.projectTransfers.create({ from_warehouse_id: whId, to_project_id: toProjectId, material_id: mid, brand_id: bid || null, unit_id: uid, quantity: n, specs: finalSpecs, note, transfer_date: toServerIso(), }, user?.project_id); toast.success("انتقال بین کارگاهی ثبت شد و از انبار مبدأ کسر گردید ✓"); notifyDataChanged("inventory"); setQty(""); setSpecsChips([]); setNote(""); setMatTxt(""); setBrandTxt(""); setUnitTxt(""); setToProjectId(""); await data.refresh(); await loadTransfers(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در ثبت انتقال بین کارگاهی"); } finally { setBusy(false); } }; const submitList = async (t: TransactionType) => { const ops: Record[] = rows .map((r, i): Record | null => { const n = parseNum(r.qty); if (!(n > 0)) return null; const rec = formRecords[i]; if (!rec) return null; if (t === "STOCK_COUNT") { return { transaction_type: "STOCK_COUNT", warehouse_id: whId, material_id: rec.material.id, brand_id: rec.brand?.id || null, physical_qty: n, note: r.note, transaction_date: toServerIso() }; } return { transaction_type: t, warehouse_id: whId, material_id: rec.material.id, brand_id: rec.brand?.id || null, quantity: n, unit_id: rec.unit?.id ?? null, specs: rec.specs || {}, note: r.note, transaction_date: toServerIso() }; }) .filter((x): x is Record => x !== null); if (!ops.length) return toast.error("هیچ ردیفی پر نشده است"); await postSeq(ops); }; interface CountConfirmRow { material: string; unit: string; warehouse: string; current: number; counted: number; variance: number; } const [countConfirm, setCountConfirm] = useState(null); const [countBusy, setCountBusy] = useState(false); const countOps = (): { ops: Record[]; confirm: CountConfirmRow[] } | null => { const ops: Record[] = []; const confirm: CountConfirmRow[] = []; rows.forEach((r, i) => { const n = parseNum(r.qty); const rec = formRecords[i]; if (!(n > 0) || !rec) return; ops.push({ transaction_type: "STOCK_COUNT", warehouse_id: whId, material_id: rec.material.id, brand_id: rec.brand?.id || null, physical_qty: n, note: r.note, transaction_date: toServerIso(), }); confirm.push({ material: rec.material.name, unit: unitName(data.lookup.units, rec.unit?.id), warehouse: whName(warehouses, whId), current: rec.quantity_number, counted: n, variance: Math.round((n - rec.quantity_number) * 10000) / 10000, }); }); if (!ops.length) return null; return { ops, confirm }; }; const requestCountConfirm = () => { const built = countOps(); if (!built) return toast.error("هیچ ردیفی پر نشده است"); setCountConfirm(built.confirm); }; const confirmCount = async () => { const built = countOps(); if (!built) return; setCountBusy(true); try { await postSeq(built.ops); setCountConfirm(null); } finally { setCountBusy(false); } }; const submitReturn = async () => { const ops: Record[] = rows .map((r, i): Record | null => { const n = parseNum(r.qty); if (!(n > 0)) return null; const t = todayWithdrawals[i]; if (!t) return null; return { transaction_type: "RETURN", warehouse_id: whId, material_id: t.material?.id, brand_id: t.brand?.id || null, quantity: n, unit_id: t.unit?.id ?? null, specs: t.specs || {}, note: r.note, transaction_date: toServerIso() }; }) .filter((x): x is Record => x !== null); if (!ops.length) return toast.error("هیچ ردیفی پر نشده است"); await postSeq(ops); }; const submitTransfer = async () => { const ops: Record[] = rows .map((r, i): Record | null => { const n = parseNum(r.qty); if (!(n > 0)) return null; const rec = formRecords[i]; if (!rec) return null; return { transaction_type: "TRANSFER_OUT", warehouse_id: fromId, dest_warehouse_id: toId, material_id: rec.material.id, brand_id: rec.brand?.id || null, quantity: n, unit_id: rec.unit?.id ?? null, specs: rec.specs || {}, note: r.note, transaction_date: toServerIso() }; }) .filter((x): x is Record => x !== null); if (!ops.length) return toast.error("هیچ ردیفی پر نشده است"); await postSeq(ops); }; const submitFound = async () => { const ops: Record[] = rows .map((r, i): Record | null => { const n = parseNum(r.qty); if (!(n > 0)) return null; const item = pendingLosses[i]; if (!item) return null; if (n > item.remaining) return null; const t = item.tx; return { transaction_type: "FOUND", warehouse_id: whId, material_id: t.material?.id, brand_id: t.brand?.id || null, quantity: n, unit_id: t.unit?.id ?? null, specs: t.specs || {}, note: r.note, transaction_date: toServerIso() }; }) .filter((x): x is Record => x !== null); if (!ops.length) return toast.error("هیچ ردیف معتبری پر نشده است"); await postSeq(ops); }; const doSubmit = () => { try { if (type === "PURCHASE") void submitPurchase(); else if (type === "PROJECT_TRANSFER") void submitProjectTransfer(); else if (type === "RETURN") void submitReturn(); else if (type === "TRANSFER") void submitTransfer(); else if (type === "FOUND") void submitFound(); else if (type === "STOCK_COUNT") requestCountConfirm(); else void submitList(type); } catch (err) { toast.error(err instanceof Error ? err.message : "خطای غیرمنتظره در ثبت"); } }; const canSubmit = (): boolean => { if (type === "PURCHASE") { return !!(whId && matTxt.trim() && brandTxt.trim() && unitTxt.trim() && parseNum(qty) > 0 && specsComplete); } if (type === "PROJECT_TRANSFER") { return !!(whId && toProjectId && matTxt.trim() && unitTxt.trim() && parseNum(qty) > 0); } if (type === "TRANSFER") return !!(fromId && toId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0)); if (type === "RETURN") return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0)); if (type === "FOUND") { return !!( whId && rows.length > 0 && rows.every((r, i) => { const n = parseNum(r.qty); return !(n > 0) || n <= (pendingLosses[i]?.remaining ?? 0); }) && rows.some((r) => parseNum(r.qty) > 0) ); } return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0)); }; useEffect(() => { if (type === "RETURN") { setRows(todayWithdrawals.map((t) => ({ key: t.inventory_id, qty: "", note: "" }))); } else if (type === "FOUND") { setRows(pendingLosses.map((x) => ({ key: x.tx.inventory_id, qty: "", note: "" }))); } else if (type === "TRANSFER") { setRows(formRecords.map((r) => ({ key: r.id, qty: "", note: "" }))); } else { setRows(formRecords.map((r) => ({ key: r.id, qty: "", note: "" }))); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [type, whId, fromId, toId, matTxt, formRecords.length, todayWithdrawals.length, pendingLosses.length]); const meta = FORM_TYPES.find((f) => f.t === type)!; if (isAdmin) { return (
🔒

دسترسی فقط‌خواندنی

نقش ادمین اجازه ثبت تغییر موجودی ندارد. این بخش برای مدیران پروژه فعال است.

); } if (!isPM) { return (
این بخش فقط برای مدیر پروژه است.
); } const isCount = type === "STOCK_COUNT"; const isTransfer = type === "TRANSFER"; return (
{/* انتخاب نوع تغییر موجودی — چیپ‌های لمسی مرتب در موبایل */}

نوع تغییر موجودی

{FORM_TYPES.map((f) => ( ))}

{meta.desc}

{/* انتخاب انبارها (چیپ‌های لمسی به جای سلکت خام) */} {type === "PROJECT_TRANSFER" ? (
{whOpts.map((w) => ( ))}
) : !isTransfer ? (
{whOpts.map((w) => ( ))}
{(type === "WITHDRAWAL" || type === "DAMAGE" || type === "WASTE" || type === "LOSS" || type === "STOCK_COUNT" || type === "RETURN" || type === "FOUND") && (
{ setMatTxt(e.target.value); setRows([]); }} /> {data.lookup.materials.map((m) =>
)}
) : (
{whOpts.map((w) => ( ))}
{whOpts.filter((w) => w.id !== fromId).map((w) => ( ))}
{ setMatTxt(e.target.value); setRows([]); }} /> {data.lookup.materials.map((m) =>
)} {/* فرم خرید / موجودی اولیه یا انتقال بین کارگاهی */} {(type === "PURCHASE" || type === "PROJECT_TRANSFER") && (
{/* چیپ‌های انتخاب سریع از موجودی‌های انبار مبدأ */} {type === "PROJECT_TRANSFER" && whId && (
📦 موجودی‌های موجود در {whName(warehouses, whId)} — برای پر کردن خودکار فرم کلیک کنید: {faNum(whStock.length)} قلم کالا
{whStock.length === 0 ? (
هیچ کالایی با موجودی مثبت در این انبار یافت نشد.
) : (
{whStock.map((r) => { const isSelected = matTxt === r.material.name && (brandTxt === (r.brand?.name || "") || (!brandTxt && !r.brand)); const specsSummary = Object.entries(r.specs || {}) .map(([k, v]) => (v ? `${v}` : k.replace(/^spec_/, ""))) .filter(Boolean) .join(" · "); return ( ); })}
)}
)}
mm.name)} allowAdd addSuffix="به عنوان متریال جدید" placeholder="نام متریال — از فهرست انتخاب یا نام جدید تایپ کنید" onInputChange={(v) => { setMatTxt(v); setRows([]); }} onChange={(it) => { setMatTxt(it.label); setRows([]); }} /> {!matByName(matTxt) && matTxt.trim() && (
«{matTxt.trim()}» در فهرست نیست — هنگام ثبت، به‌صورت خودکار در کاتالوگ سراسری ساخته می‌شود.
)}
b.name)} allowAdd addSuffix="به عنوان برند جدید" placeholder={type === "PROJECT_TRANSFER" ? "نام برند (اختیاری — برای اقلام بدون برند خالی بگذارید)" : "نام برند — از فهرست انتخاب یا نام جدید تایپ کنید"} onInputChange={setBrandTxt} onChange={(it) => setBrandTxt(it.label)} />
u.name_fa || u.code)} allowAdd addSuffix="به عنوان واحد جدید" placeholder="واحد اندازه‌گیری — از فهرست یا نام جدید" onInputChange={setUnitTxt} onChange={(it) => setUnitTxt(it.label)} />
setQty(e.target.value)} />
setDateInput(iso ?? new Date().toISOString())} placeholder="۱۴۰۳/۰۵/۱۲" withTime />