fix(inventory): adjustment form - transfer rows, safe date handling, Shamsi date picker, drop new-item category field
This commit is contained in:
@@ -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<string>(() => 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<string, string> = {};
|
||||
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<string, unknown> = {
|
||||
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<string, unknown> => 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<string, unknown> => 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<string, unknown> => 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<string, unknown> => 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() {
|
||||
<Header
|
||||
title="تغییر موجودی"
|
||||
sub={user?.project_name || ""}
|
||||
menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />}
|
||||
actions={<InventoryTopNav pathname={location.pathname} onLogout={logout} />}
|
||||
/>
|
||||
<PageLayout className="flex flex-col gap-3">
|
||||
{/* انتخاب نوع تراکنش */}
|
||||
@@ -444,26 +457,19 @@ export function ChangeInventoryPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>تاریخ و زمان</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm ltr"
|
||||
<label>تاریخ و زمان (شمسی)</label>
|
||||
<JalaliDatePicker
|
||||
value={dateInput}
|
||||
onChange={(e) => setDateInput(e.target.value)}
|
||||
onChange={(iso) => setDateInput(iso ?? new Date().toISOString())}
|
||||
placeholder="۱۴۰۳/۰۵/۱۲"
|
||||
withTime
|
||||
/>
|
||||
</div>
|
||||
{!matByName(matTxt) && matTxt.trim() && (
|
||||
<div className="field md:col-span-2">
|
||||
<label>دستهبندی (کالای جدید) *</label>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm"
|
||||
value={catId}
|
||||
onChange={(e) => setCatId(e.target.value)}
|
||||
>
|
||||
<option value="">— انتخاب کنید —</option>
|
||||
{data.lookup.categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
<div className="mt-1 text-[11px] text-muted">این متریال در فهرست نیست؛ فقط برای کالای جدید دستهبندی لازم است.</div>
|
||||
<div className="rounded-lg border border-amber/30 bg-amber/5 px-3 py-2 text-[12px] text-amber">
|
||||
این متریال در فهرست نیست — ابتدا متریال را بسازید (مثلاً هنگام ثبت پیشنهاد در دفتر تدارکات)، سپس خرید را ثبت کنید.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="field md:col-span-2">
|
||||
|
||||
Reference in New Issue
Block a user