diff --git a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx index 0073740..6037796 100644 --- a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx +++ b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx @@ -14,11 +14,11 @@ import { uniqueUnits } from "../../lib/units"; import type { InventoryTransaction, TransactionType } from "../../lib/types"; import { faNum } from "../../lib/formatters"; import { gregorianToJalali, MONTHS, toFa } from "../../lib/jalali"; +import { cn } from "../../lib/utils"; type FormType = TransactionType | "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: "RETURN", label: "برگشت", dir: "افزایش", cls: "in", color: "#10b981", desc: "برگشت از مصالحی که امروز برداشت شده‌اند — نام متریال را تایپ کنید و مقدار برگشتی هر ردیف را بزنید." }, @@ -46,7 +46,6 @@ function jalDateLabel(d: Date): string { return `${toFa(jd)} ${MONTHS[jm - 1]} ${toFa(jy)}`; } -// تغییر موجودی — پورت کامل صفحه‌ی ارسالی (همه انواع تراکنش) export function ChangeInventoryPage() { const user = useAuthStore((s) => s.user); const data = useInventoryData(); @@ -60,15 +59,12 @@ export function ChangeInventoryPage() { const [brandTxt, setBrandTxt] = useState(""); const [unitTxt, setUnitTxt] = useState(""); const [qty, setQty] = useState(""); - // تاریخ و ساعت به صورت ISO (تقویم شمسی — JalaliDatePicker) const [dateInput, setDateInput] = useState(() => new Date().toISOString()); - // مشخصه‌های فنی — چیپ‌های چندتایی، دقیقاً مثل فیلد «مشخصه» در ویزارد دفتر تدارکات const [specsChips, setSpecsChips] = useState([]); const [note, setNote] = useState(""); const [rows, setRows] = useState([]); const [busy, setBusy] = useState(false); - // prefill از URL (پرش از کنترل روزانه) useEffect(() => { const t = params.get("type"); const w = params.get("wh"); @@ -91,7 +87,6 @@ export function ChangeInventoryPage() { 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; @@ -105,8 +100,6 @@ export function ChangeInventoryPage() { 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; }; - // تبدیل امن تاریخ فرم به ISO — اگر فیلد خالی/نامعتبر بود (که قبلاً submit را - // بی‌صدا می‌شکست) به «هم‌اکنون» برمی‌گردد تا ثبت هیچ‌وقت قفل نشود. const toServerIso = () => { try { const d = new Date(dateInput); @@ -115,8 +108,6 @@ export function ChangeInventoryPage() { return new Date().toISOString(); }; - // هر چیپ یک مشخصه است؛ در specs انبار به‌صورت کلید با مقدار خالی ذخیره می‌شود - // تا رکوردهای انبارِ دارای مشخصه‌های متفاوت از هم جدا بمانند const specsFromChips = () => { const out: Record = {}; specsChips.forEach((c) => { @@ -151,9 +142,6 @@ export function ChangeInventoryPage() { .sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime()); }, [data.transactions, whId, matTxt]); - // --- پیدا شده: مانده‌ی مفقودی هر ردیف = مفقودی − مجموع پیداشده‌های قبلیِ همان رکورد - const lossKey = (t: InventoryTransaction) => - [t.warehouse_id, t.material?.id || "", t.brand?.id || "", JSON.stringify(t.specs || {})].join("|"); const foundRemaining = useMemo(() => { const foundByKey = new Map(); data.transactions @@ -170,7 +158,9 @@ export function ChangeInventoryPage() { // 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 @@ -188,7 +178,6 @@ export function ChangeInventoryPage() { try { for (const op of ops) await postOne(op); toast.success("ثبت شد ✓"); - // اعلام به تب‌های دیگر (داشبورد/کنترل روزانه/... باز خودشان را تازه می‌کنند) notifyDataChanged("inventory"); setRows([]); setMatTxt(""); @@ -205,7 +194,6 @@ export function ChangeInventoryPage() { } }; - // ---------- submit handlers ---------- const submitPurchase = async () => { const matName = matTxt.trim(); const brandName = brandTxt.trim(); @@ -216,10 +204,9 @@ export function ChangeInventoryPage() { if (!brandName) return toast.error("نام برند را وارد کنید"); if (!unitName) return toast.error("نام واحد را وارد کنید"); if (!(n > 0)) return toast.error("تعداد باید بزرگ‌تر از صفر باشد"); - if (!specsComplete) return toast.error("حداقل یک مشخصه فنی ثبت کنید (نام را بنویسید و Enter بزنید)"); + if (!specsComplete) return toast.error("حداقل یک مشخصه فنی ثبت کنید"); try { - // ساختِ به‌هنگام موجودیت‌های ناشناخته در کاتالوگ سراسری (بدون پیش‌نیاز تدارکات) let uid = unitIdByName(unitName); if (!uid) { const created = await apiV26.catalog.createUnit(unitName); @@ -264,7 +251,6 @@ export function ChangeInventoryPage() { await postSeq(ops); }; - // ---------- شمارش موجودی: پیش‌نمایش اختلاف + تأیید قبل از ثبت ---------- interface CountConfirmRow { material: string; unit: string; @@ -358,7 +344,7 @@ export function ChangeInventoryPage() { if (!(n > 0)) return null; const item = pendingLosses[i]; if (!item) return null; - if (n > item.remaining) 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() }; }) @@ -382,12 +368,10 @@ export function ChangeInventoryPage() { const canSubmit = (): boolean => { if (type === "PURCHASE") { - // نام‌های تایپ‌شده لازم نیست از قبل در فهرست باشند — در submit ساخته می‌شوند return !!(whId && matTxt.trim() && brandTxt.trim() && unitTxt.trim() && parseNum(qty) > 0 && specsComplete); } 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 && @@ -402,7 +386,6 @@ export function ChangeInventoryPage() { return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0)); }; - // sync rows when the record list changes useEffect(() => { if (type === "RETURN") { setRows(todayWithdrawals.map((t) => ({ key: t.inventory_id, qty: "", note: "" }))); @@ -447,54 +430,68 @@ export function ChangeInventoryPage() { const isTransfer = type === "TRANSFER"; return ( -
+
- - {/* انتخاب نوع تراکنش */} -
+ + {/* انتخاب نوع تغییر موجودی — چیپ‌های لمسی مرتب در موبایل */} +

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

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

{meta.desc}

+

{meta.desc}

-
- {/* انتخاب انبارها */} +
+ {/* انتخاب انبارها (چیپ‌های لمسی به جای سلکت خام) */} {!isTransfer ? ( -
-
- - +
+
+ +
+ {whOpts.map((w) => ( + + ))} +
+ {(type === "WITHDRAWAL" || type === "DAMAGE" || type === "WASTE" || type === "LOSS" || type === "STOCK_COUNT" || type === "RETURN" || type === "FOUND") && (
- + { setMatTxt(e.target.value); setRows([]); }} @@ -508,33 +505,51 @@ export function ChangeInventoryPage() { ) : (
- - + +
+ {whOpts.map((w) => ( + + ))} +
- - + +
+ {whOpts.filter((w) => w.id !== fromId).map((w) => ( + + ))} +
- + { setMatTxt(e.target.value); setRows([]); }} @@ -550,7 +565,7 @@ export function ChangeInventoryPage() { {type === "PURCHASE" && (
- + mm.name)} @@ -567,7 +582,7 @@ export function ChangeInventoryPage() { )}
- + b.name)} @@ -579,7 +594,7 @@ export function ChangeInventoryPage() { />
- + u.name_fa || u.code)} @@ -591,17 +606,17 @@ export function ChangeInventoryPage() { />
- + setQty(e.target.value)} />
- + setDateInput(iso ?? new Date().toISOString())} @@ -610,7 +625,7 @@ export function ChangeInventoryPage() { />
- +
- +