feat(warehouse): wizard-style spec chips + unique unit options
- «مشخصه فنی» in موجودی اولیه is now the same MultiSelectCombobox chip control as the wizard's «مشخصه» (name-only chips, stored as spec keys with empty values so stock variants stay separate). - Unit combobox options deduped via uniqueUnits (canonical base preferred). - Description/validation copy updated accordingly.
This commit is contained in:
@@ -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<string>(() => new Date().toISOString());
|
||||
const [specsRows, setSpecsRows] = useState<{ rid: number; label: string; value: string }[]>([
|
||||
{ rid: 1, label: "", value: "" },
|
||||
]);
|
||||
// مشخصههای فنی — چیپهای چندتایی، دقیقاً مثل فیلد «مشخصه» در ویزارد دفتر تدارکات
|
||||
const [specsChips, setSpecsChips] = useState<string[]>([]);
|
||||
const [note, setNote] = useState("");
|
||||
const [rows, setRows] = useState<RowDraft[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -114,14 +115,17 @@ export function ChangeInventoryPage() {
|
||||
return new Date().toISOString();
|
||||
};
|
||||
|
||||
const specsFromRows = () => {
|
||||
// هر چیپ یک مشخصه است؛ در specs انبار بهصورت کلید با مقدار خالی ذخیره میشود
|
||||
// تا رکوردهای انبارِ دارای مشخصههای متفاوت از هم جدا بمانند
|
||||
const specsFromChips = () => {
|
||||
const out: Record<string, string> = {};
|
||||
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<string, unknown> = {
|
||||
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() {
|
||||
<label>واحد *</label>
|
||||
<Combobox
|
||||
value={unitTxt}
|
||||
items={data.lookup.units.map((u) => 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() {
|
||||
</div>
|
||||
<div className="field md:col-span-2">
|
||||
<label>مشخصههای فنی *</label>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{specsRows.map((r) => (
|
||||
<div key={r.rid} className="flex gap-2">
|
||||
<input
|
||||
className="h-8 w-1/2 rounded-lg border border-line bg-panel px-3 text-sm"
|
||||
placeholder="نام مشخصه (مثلاً سایز)"
|
||||
value={r.label}
|
||||
onChange={(e) => setSpecsRows((rs) => rs.map((x) => (x.rid === r.rid ? { ...x, label: e.target.value } : x)))}
|
||||
<MultiSelectCombobox
|
||||
values={specsChips}
|
||||
onChange={setSpecsChips}
|
||||
placeholder="نام مشخصه را تایپ کنید و Enter بزنید (مثلاً سایز)"
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
className="h-8 w-1/2 rounded-lg border border-line bg-panel px-3 text-sm"
|
||||
placeholder="مقدار (مثلاً ۱۶)"
|
||||
value={r.value}
|
||||
onChange={(e) => setSpecsRows((rs) => rs.map((x) => (x.rid === r.rid ? { ...x, value: e.target.value } : x)))}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 px-2 text-muted hover:text-red"
|
||||
onClick={() => setSpecsRows((rs) => (rs.length > 1 ? rs.filter((x) => x.rid !== r.rid) : rs))}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="w-fit rounded-lg border border-dashed border-accent/50 px-3 py-1 text-[12px] text-accent"
|
||||
onClick={() => setSpecsRows((rs) => [...rs, { rid: Date.now(), label: "", value: "" }])}
|
||||
>
|
||||
+ افزودن مشخصه فنی
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field md:col-span-2">
|
||||
<label>یادداشت</label>
|
||||
|
||||
Reference in New Issue
Block a user