diff --git a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx index 6602b7b..0073740 100644 --- a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx +++ b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx @@ -3,12 +3,14 @@ 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 { 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 } from "../../lib/types"; import { faNum } from "../../lib/formatters"; import { gregorianToJalali, MONTHS, toFa } from "../../lib/jalali"; @@ -17,7 +19,7 @@ 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: "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: "برگشت از مصالحی که امروز برداشت شده‌اند — نام متریال را تایپ کنید و مقدار برگشتی هر ردیف را بزنید." }, { t: "TRANSFER", label: "انتقال", dir: "جابه‌جایی", cls: "mv", color: "#2563eb", desc: "جابه‌جایی بین انبارهای پروژه — از انبار مبدأ، مقداری از هر مدل را به انبار مقصد منتقل کنید." }, @@ -60,9 +62,8 @@ export function ChangeInventoryPage() { const [qty, setQty] = useState(""); // تاریخ و ساعت به صورت ISO (تقویم شمسی — JalaliDatePicker) const [dateInput, setDateInput] = useState(() => new Date().toISOString()); - const [specsRows, setSpecsRows] = useState<{ rid: number; label: string; value: string }[]>([ - { rid: 1, label: "", value: "" }, - ]); + // مشخصه‌های فنی — چیپ‌های چندتایی، دقیقاً مثل فیلد «مشخصه» در ویزارد دفتر تدارکات + const [specsChips, setSpecsChips] = useState([]); const [note, setNote] = useState(""); const [rows, setRows] = useState([]); const [busy, setBusy] = useState(false); @@ -114,14 +115,17 @@ export function ChangeInventoryPage() { return new Date().toISOString(); }; - const specsFromRows = () => { + // هر چیپ یک مشخصه است؛ در specs انبار به‌صورت کلید با مقدار خالی ذخیره می‌شود + // تا رکوردهای انبارِ دارای مشخصه‌های متفاوت از هم جدا بمانند + const specsFromChips = () => { const out: Record = {}; - specsRows.forEach((r) => { - if (r.label.trim() && r.value.trim()) out[r.label.trim()] = r.value.trim(); + specsChips.forEach((c) => { + const k = c.trim(); + if (k) out[k] = ""; }); return out; }; - const specsComplete = specsRows.some((r) => r.label.trim() && r.value.trim()); + const specsComplete = specsChips.some((c) => c.trim()); const todayWithdrawals = useMemo(() => { const tk = todayKey(); @@ -191,7 +195,7 @@ export function ChangeInventoryPage() { setBrandTxt(""); setUnitTxt(""); setQty(""); - setSpecsRows([{ rid: Date.now(), label: "", value: "" }]); + setSpecsChips([]); setNote(""); await data.refresh(); } catch (err) { @@ -212,7 +216,7 @@ export function ChangeInventoryPage() { if (!brandName) return toast.error("نام برند را وارد کنید"); if (!unitName) return toast.error("نام واحد را وارد کنید"); if (!(n > 0)) return toast.error("تعداد باید بزرگ‌تر از صفر باشد"); - if (!specsComplete) return toast.error("حداقل یک مشخصه فنی (نام و مقدار) ثبت کنید"); + if (!specsComplete) return toast.error("حداقل یک مشخصه فنی ثبت کنید (نام را بنویسید و Enter بزنید)"); try { // ساختِ به‌هنگام موجودیت‌های ناشناخته در کاتالوگ سراسری (بدون پیش‌نیاز تدارکات) @@ -233,7 +237,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, + quantity: n, unit_id: uid, specs: specsFromChips(), note, transaction_date: toServerIso(), }; await postSeq([op]); @@ -578,7 +582,7 @@ export function ChangeInventoryPage() { u.name_fa || u.code)} + items={uniqueUnits(data.lookup.units).map((u) => u.name_fa || u.code)} allowAdd addSuffix="به عنوان واحد جدید" placeholder="واحد اندازه‌گیری — از فهرست یا نام جدید" @@ -607,38 +611,12 @@ export function ChangeInventoryPage() {
-
- {specsRows.map((r) => ( -
- setSpecsRows((rs) => rs.map((x) => (x.rid === r.rid ? { ...x, label: e.target.value } : x)))} - /> - setSpecsRows((rs) => rs.map((x) => (x.rid === r.rid ? { ...x, value: e.target.value } : x)))} - /> - -
- ))} - -
+