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 { useAuthStore } from "../../store/auth";
|
||||||
import { PanelMenu } from "../../components/PanelMenu";
|
import { PanelMenu } from "../../components/PanelMenu";
|
||||||
import { panelMenuItems, useLogout } from "./panelMenu";
|
import { panelMenuItems, useLogout } from "./panelMenu";
|
||||||
|
import { InventoryTopNav } from "./InventoryTopNav";
|
||||||
import { apiV26 } from "../../lib/api";
|
import { apiV26 } from "../../lib/api";
|
||||||
import { useInventoryData, unitName } from "./useInventoryData";
|
import { useInventoryData, unitName } from "./useInventoryData";
|
||||||
|
import { JalaliDatePicker } from "../../components/JalaliDatePicker";
|
||||||
import type { InventoryTransaction, TransactionType } from "../../lib/types";
|
import type { InventoryTransaction, TransactionType } from "../../lib/types";
|
||||||
import { faNum } from "../../lib/formatters";
|
import { faNum } from "../../lib/formatters";
|
||||||
import { gregorianToJalali, toFa } from "../../lib/jalali";
|
import { gregorianToJalali, toFa } from "../../lib/jalali";
|
||||||
@@ -54,15 +56,11 @@ export function ChangeInventoryPage() {
|
|||||||
const [fromId, setFromId] = useState("");
|
const [fromId, setFromId] = useState("");
|
||||||
const [toId, setToId] = useState("");
|
const [toId, setToId] = useState("");
|
||||||
const [matTxt, setMatTxt] = useState("");
|
const [matTxt, setMatTxt] = useState("");
|
||||||
const [catId, setCatId] = useState("");
|
|
||||||
const [brandTxt, setBrandTxt] = useState("");
|
const [brandTxt, setBrandTxt] = useState("");
|
||||||
const [unitTxt, setUnitTxt] = useState("");
|
const [unitTxt, setUnitTxt] = useState("");
|
||||||
const [qty, setQty] = useState("");
|
const [qty, setQty] = useState("");
|
||||||
const [dateInput, setDateInput] = useState(() => {
|
// تاریخ و ساعت به صورت ISO (تقویم شمسی — JalaliDatePicker)
|
||||||
const d = new Date();
|
const [dateInput, setDateInput] = useState<string>(() => new Date().toISOString());
|
||||||
d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
|
|
||||||
return d.toISOString().slice(0, 16);
|
|
||||||
});
|
|
||||||
const [specsRows, setSpecsRows] = useState<{ rid: number; label: string; value: string }[]>([
|
const [specsRows, setSpecsRows] = useState<{ rid: number; label: string; value: string }[]>([
|
||||||
{ rid: 1, label: "", value: "" },
|
{ rid: 1, label: "", value: "" },
|
||||||
]);
|
]);
|
||||||
@@ -82,7 +80,7 @@ export function ChangeInventoryPage() {
|
|||||||
}
|
}
|
||||||
}, [params]);
|
}, [params]);
|
||||||
|
|
||||||
const isAdmin = user?.role === "admin";
|
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager";
|
||||||
const isPM = user?.role === "pm";
|
const isPM = user?.role === "pm";
|
||||||
|
|
||||||
const warehouses = data.warehouses;
|
const warehouses = data.warehouses;
|
||||||
@@ -93,18 +91,30 @@ export function ChangeInventoryPage() {
|
|||||||
|
|
||||||
const formRecords = useMemo(() => {
|
const formRecords = useMemo(() => {
|
||||||
const txt = matTxt.trim();
|
const txt = matTxt.trim();
|
||||||
|
// انتقال از انبار مبدأ استفاده میکند، بقیه از انبار انتخابی
|
||||||
|
const wid = type === "TRANSFER" ? fromId : whId;
|
||||||
return data.inventory.filter((r) => {
|
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;
|
if (txt && !r.material.name.includes(txt)) return false;
|
||||||
return true;
|
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 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 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 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 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 specsFromRows = () => {
|
||||||
const out: Record<string, string> = {};
|
const out: Record<string, string> = {};
|
||||||
specsRows.forEach((r) => {
|
specsRows.forEach((r) => {
|
||||||
@@ -170,7 +180,6 @@ export function ChangeInventoryPage() {
|
|||||||
const n = parseNum(qty);
|
const n = parseNum(qty);
|
||||||
if (!whId) return toast.error("انبار را انتخاب کنید");
|
if (!whId) return toast.error("انبار را انتخاب کنید");
|
||||||
if (!m) return toast.error("متریال در فهرست نیست — نام را دقیق تایپ کنید");
|
if (!m) return toast.error("متریال در فهرست نیست — نام را دقیق تایپ کنید");
|
||||||
if (!m.category && !catId) return toast.error("برای کالای جدید، دستهبندی را انتخاب کنید");
|
|
||||||
if (!bid) return toast.error("برند را انتخاب کنید");
|
if (!bid) return toast.error("برند را انتخاب کنید");
|
||||||
if (!uid) return toast.error("واحد را انتخاب کنید");
|
if (!uid) return toast.error("واحد را انتخاب کنید");
|
||||||
if (!(n > 0)) return toast.error("تعداد باید بزرگتر از صفر باشد");
|
if (!(n > 0)) return toast.error("تعداد باید بزرگتر از صفر باشد");
|
||||||
@@ -178,7 +187,7 @@ export function ChangeInventoryPage() {
|
|||||||
const op: Record<string, unknown> = {
|
const op: Record<string, unknown> = {
|
||||||
transaction_type: "PURCHASE", warehouse_id: whId, material_id: m.id, brand_id: bid,
|
transaction_type: "PURCHASE", warehouse_id: whId, material_id: m.id, brand_id: bid,
|
||||||
quantity: n, unit_id: uid, specs: specsFromRows(), note,
|
quantity: n, unit_id: uid, specs: specsFromRows(), note,
|
||||||
transaction_date: new Date(dateInput).toISOString(),
|
transaction_date: toServerIso(),
|
||||||
};
|
};
|
||||||
await postSeq([op]);
|
await postSeq([op]);
|
||||||
};
|
};
|
||||||
@@ -192,9 +201,9 @@ export function ChangeInventoryPage() {
|
|||||||
const rec = formRecords[i];
|
const rec = formRecords[i];
|
||||||
if (!rec) return null;
|
if (!rec) return null;
|
||||||
if (isCount) {
|
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);
|
.filter((x): x is Record<string, unknown> => x !== null);
|
||||||
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
||||||
@@ -208,7 +217,7 @@ export function ChangeInventoryPage() {
|
|||||||
if (!(n > 0)) return null;
|
if (!(n > 0)) return null;
|
||||||
const t = todayWithdrawals[i];
|
const t = todayWithdrawals[i];
|
||||||
if (!t) return null;
|
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);
|
.filter((x): x is Record<string, unknown> => x !== null);
|
||||||
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
||||||
@@ -222,7 +231,7 @@ export function ChangeInventoryPage() {
|
|||||||
if (!(n > 0)) return null;
|
if (!(n > 0)) return null;
|
||||||
const rec = formRecords[i];
|
const rec = formRecords[i];
|
||||||
if (!rec) return null;
|
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);
|
.filter((x): x is Record<string, unknown> => x !== null);
|
||||||
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
||||||
@@ -236,7 +245,7 @@ export function ChangeInventoryPage() {
|
|||||||
if (!(n > 0)) return null;
|
if (!(n > 0)) return null;
|
||||||
const t = lossTxs[i];
|
const t = lossTxs[i];
|
||||||
if (!t) return null;
|
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);
|
.filter((x): x is Record<string, unknown> => x !== null);
|
||||||
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
if (!ops.length) return toast.error("هیچ ردیفی پر نشده است");
|
||||||
@@ -244,11 +253,15 @@ export function ChangeInventoryPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const doSubmit = () => {
|
const doSubmit = () => {
|
||||||
|
try {
|
||||||
if (type === "PURCHASE") void submitPurchase();
|
if (type === "PURCHASE") void submitPurchase();
|
||||||
else if (type === "RETURN") void submitReturn();
|
else if (type === "RETURN") void submitReturn();
|
||||||
else if (type === "TRANSFER") void submitTransfer();
|
else if (type === "TRANSFER") void submitTransfer();
|
||||||
else if (type === "FOUND") void submitFound();
|
else if (type === "FOUND") void submitFound();
|
||||||
else void submitList(type);
|
else void submitList(type);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : "خطای غیرمنتظره در ثبت");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const canSubmit = (): boolean => {
|
const canSubmit = (): boolean => {
|
||||||
@@ -310,7 +323,7 @@ export function ChangeInventoryPage() {
|
|||||||
<Header
|
<Header
|
||||||
title="تغییر موجودی"
|
title="تغییر موجودی"
|
||||||
sub={user?.project_name || ""}
|
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">
|
<PageLayout className="flex flex-col gap-3">
|
||||||
{/* انتخاب نوع تراکنش */}
|
{/* انتخاب نوع تراکنش */}
|
||||||
@@ -444,26 +457,19 @@ export function ChangeInventoryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>تاریخ و زمان</label>
|
<label>تاریخ و زمان (شمسی)</label>
|
||||||
<input
|
<JalaliDatePicker
|
||||||
type="datetime-local"
|
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm ltr"
|
|
||||||
value={dateInput}
|
value={dateInput}
|
||||||
onChange={(e) => setDateInput(e.target.value)}
|
onChange={(iso) => setDateInput(iso ?? new Date().toISOString())}
|
||||||
|
placeholder="۱۴۰۳/۰۵/۱۲"
|
||||||
|
withTime
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!matByName(matTxt) && matTxt.trim() && (
|
{!matByName(matTxt) && matTxt.trim() && (
|
||||||
<div className="field md:col-span-2">
|
<div className="field md:col-span-2">
|
||||||
<label>دستهبندی (کالای جدید) *</label>
|
<div className="rounded-lg border border-amber/30 bg-amber/5 px-3 py-2 text-[12px] text-amber">
|
||||||
<select
|
این متریال در فهرست نیست — ابتدا متریال را بسازید (مثلاً هنگام ثبت پیشنهاد در دفتر تدارکات)، سپس خرید را ثبت کنید.
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm"
|
</div>
|
||||||
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="field md:col-span-2">
|
<div className="field md:col-span-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user