From 0774f088233a5ab055ede13da3273a719969e1ca Mon Sep 17 00:00:00 2001 From: ali_mohaghegh Date: Thu, 27 Aug 2026 15:04:25 +0330 Subject: [PATCH] fix(inventory): adjustment form - transfer rows, safe date handling, Shamsi date picker, drop new-item category field --- .../pages/Inventory/ChangeInventoryPage.tsx | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx index 54d7de5..d4e94c5 100644 --- a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx +++ b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx @@ -5,8 +5,10 @@ import { Header, PageLayout } from "../../components/layout/Header"; import { useAuthStore } from "../../store/auth"; import { PanelMenu } from "../../components/PanelMenu"; import { panelMenuItems, useLogout } from "./panelMenu"; +import { InventoryTopNav } from "./InventoryTopNav"; import { apiV26 } from "../../lib/api"; import { useInventoryData, unitName } from "./useInventoryData"; +import { JalaliDatePicker } from "../../components/JalaliDatePicker"; import type { InventoryTransaction, TransactionType } from "../../lib/types"; import { faNum } from "../../lib/formatters"; import { gregorianToJalali, toFa } from "../../lib/jalali"; @@ -54,15 +56,11 @@ export function ChangeInventoryPage() { const [fromId, setFromId] = useState(""); const [toId, setToId] = useState(""); const [matTxt, setMatTxt] = useState(""); - const [catId, setCatId] = useState(""); const [brandTxt, setBrandTxt] = useState(""); const [unitTxt, setUnitTxt] = useState(""); const [qty, setQty] = useState(""); - const [dateInput, setDateInput] = useState(() => { - const d = new Date(); - d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); - return d.toISOString().slice(0, 16); - }); + // تاریخ و ساعت به صورت ISO (تقویم شمسی — JalaliDatePicker) + const [dateInput, setDateInput] = useState(() => new Date().toISOString()); const [specsRows, setSpecsRows] = useState<{ rid: number; label: string; value: string }[]>([ { rid: 1, label: "", value: "" }, ]); @@ -82,7 +80,7 @@ export function ChangeInventoryPage() { } }, [params]); - const isAdmin = user?.role === "admin"; + const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; const isPM = user?.role === "pm"; const warehouses = data.warehouses; @@ -93,18 +91,30 @@ 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 !== whId) return false; + if (r.warehouse?.id !== wid) return false; if (txt && !r.material.name.includes(txt)) return false; return true; }); - }, [data.inventory, whId, matTxt]); + }, [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; }; + // تبدیل امن تاریخ فرم به ISO — اگر فیلد خالی/نامعتبر بود (که قبلاً submit را + // بی‌صدا می‌شکست) به «هم‌اکنون» برمی‌گردد تا ثبت هیچ‌وقت قفل نشود. + const toServerIso = () => { + try { + const d = new Date(dateInput); + if (!Number.isNaN(d.getTime())) return d.toISOString(); + } catch { /* نادیده */ } + return new Date().toISOString(); + }; + const specsFromRows = () => { const out: Record = {}; specsRows.forEach((r) => { @@ -170,7 +180,6 @@ export function ChangeInventoryPage() { const n = parseNum(qty); if (!whId) return toast.error("انبار را انتخاب کنید"); if (!m) return toast.error("متریال در فهرست نیست — نام را دقیق تایپ کنید"); - if (!m.category && !catId) return toast.error("برای کالای جدید، دسته‌بندی را انتخاب کنید"); if (!bid) return toast.error("برند را انتخاب کنید"); if (!uid) return toast.error("واحد را انتخاب کنید"); if (!(n > 0)) return toast.error("تعداد باید بزرگ‌تر از صفر باشد"); @@ -178,7 +187,7 @@ export function ChangeInventoryPage() { const op: Record = { transaction_type: "PURCHASE", warehouse_id: whId, material_id: m.id, brand_id: bid, quantity: n, unit_id: uid, specs: specsFromRows(), note, - transaction_date: new Date(dateInput).toISOString(), + transaction_date: toServerIso(), }; await postSeq([op]); }; @@ -192,9 +201,9 @@ export function ChangeInventoryPage() { const rec = formRecords[i]; if (!rec) return null; if (isCount) { - 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: new Date(dateInput).toISOString() }; + 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: new Date(dateInput).toISOString() }; + 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("هیچ ردیفی پر نشده است"); @@ -208,7 +217,7 @@ export function ChangeInventoryPage() { 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: new Date(dateInput).toISOString() }; + 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("هیچ ردیفی پر نشده است"); @@ -222,7 +231,7 @@ export function ChangeInventoryPage() { 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: new Date(dateInput).toISOString() }; + 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("هیچ ردیفی پر نشده است"); @@ -236,7 +245,7 @@ export function ChangeInventoryPage() { if (!(n > 0)) return null; const t = lossTxs[i]; if (!t) return null; - 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: new Date(dateInput).toISOString() }; + 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("هیچ ردیفی پر نشده است"); @@ -244,11 +253,15 @@ export function ChangeInventoryPage() { }; const doSubmit = () => { - if (type === "PURCHASE") void submitPurchase(); - else if (type === "RETURN") void submitReturn(); - else if (type === "TRANSFER") void submitTransfer(); - else if (type === "FOUND") void submitFound(); - else void submitList(type); + try { + if (type === "PURCHASE") void submitPurchase(); + else if (type === "RETURN") void submitReturn(); + else if (type === "TRANSFER") void submitTransfer(); + else if (type === "FOUND") void submitFound(); + else void submitList(type); + } catch (err) { + toast.error(err instanceof Error ? err.message : "خطای غیرمنتظره در ثبت"); + } }; const canSubmit = (): boolean => { @@ -310,7 +323,7 @@ export function ChangeInventoryPage() {
} + actions={} /> {/* انتخاب نوع تراکنش */} @@ -444,26 +457,19 @@ export function ChangeInventoryPage() { />
- - تاریخ و زمان (شمسی) + setDateInput(e.target.value)} + onChange={(iso) => setDateInput(iso ?? new Date().toISOString())} + placeholder="۱۴۰۳/۰۵/۱۲" + withTime />
{!matByName(matTxt) && matTxt.trim() && (
- - -
این متریال در فهرست نیست؛ فقط برای کالای جدید دسته‌بندی لازم است.
+
+ این متریال در فهرست نیست — ابتدا متریال را بسازید (مثلاً هنگام ثبت پیشنهاد در دفتر تدارکات)، سپس خرید را ثبت کنید. +
)}