fix(wizard): global material suggestions + unit create-option fixes
- متریال field suggests unique BASE material names from the whole catalog (brand/spec prefixes stripped; «راجین سیم مسی» → «سیم») — brand/specs have their own columns. - Save path reuses an existing material by full name only (no category equality constraint, which never matched topic-named categories) so selecting an existing material never duplicates it. - Unit field: suggestion list no longer includes the row's own typed text, which previously suppressed the create option; typing an unknown unit now shows «+ [value] به عنوان واحد جدید» (registered immediately via POST /units and added to suggestions).
This commit is contained in:
@@ -6,7 +6,7 @@ import { specsKey } from "../../lib/quoteUtils";
|
||||
import { Combobox } from "../../components/Combobox";
|
||||
import { NumberInput } from "../../components/NumberInput";
|
||||
import { MultiSelectCombobox } from "../../components/MultiSelectCombobox";
|
||||
import { api } from "../../lib/api";
|
||||
import { api, apiV26 } from "../../lib/api";
|
||||
|
||||
export interface ItemDraft {
|
||||
tempId: string;
|
||||
@@ -41,6 +41,25 @@ export function itemRowLabel(it: ItemDraft): string {
|
||||
return qtyPart ? `${name} -- ${qtyPart}` : name;
|
||||
}
|
||||
|
||||
// نام پایهی متریال از نامِ ذخیرهشده: پیشوند برند و پسوند مشخصه حذف میشوند
|
||||
// («راجین سیم مسی» → «سیم») — پیشنهادهای فیلد «متریال» فقط خودِ متریال را نشان
|
||||
// میدهند؛ برند و مشخصه ستونهای جداگانه دارند.
|
||||
function materialBaseName(name: string, brand: string, specs: string[]): string {
|
||||
let base = (name || "").trim();
|
||||
if (!base) return "";
|
||||
if (brand) {
|
||||
if (base.startsWith(brand)) base = base.slice(brand.length);
|
||||
else if (base.includes(brand)) base = base.split(brand).join(" ");
|
||||
}
|
||||
specs
|
||||
.filter((s) => s && s.trim())
|
||||
.forEach((s) => {
|
||||
if (base.includes(s)) base = base.split(s).join(" ");
|
||||
});
|
||||
base = base.replace(/\s+/g, " ").trim();
|
||||
return base || (name || "").trim();
|
||||
}
|
||||
|
||||
// مقادیر مجزای category/brand/spec داخل تاپیک — با فیلتر پلهبهپله (تاپیک → دسته → برند)
|
||||
// + مقادیر تایپشده در همهی سطرهای قبلی (suggestion از همهی فیلدها)
|
||||
// نکته: برند و واحد از همهی دستهها نمایش داده میشوند (issue #7)
|
||||
@@ -97,13 +116,24 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
||||
items.forEach((it) => { if (it.unit) all.add(it.unit); });
|
||||
return [...all];
|
||||
});
|
||||
// واحدهای تایپشده در سطرهای قبلی
|
||||
// واحدهای موجود (دیتابیس) + واحدهای تایپشده در سطرهای دیگر
|
||||
const allUnits = useMemo(() => {
|
||||
const out = new Set(unitOptions);
|
||||
items.forEach((it) => { if (it.unit) out.add(it.unit); });
|
||||
return [...out];
|
||||
}, [items, unitOptions]);
|
||||
|
||||
// نامهای پایهی یکتای همهی متریالهای کاتالوگ سراسری (فقط خود متریال،
|
||||
// بدون برند/مشخصه — آنها ستونهای خودشان را دارند)
|
||||
const allMaterialNames = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
materials.forEach((m) => {
|
||||
const base = materialBaseName(m.name, m.brand, m.specs);
|
||||
if (base) set.add(base);
|
||||
});
|
||||
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
||||
}, [materials]);
|
||||
|
||||
// برندهای ثبتشده در بخش برندها (بکاند) — بهعلاوهی برندهای متریالها
|
||||
const [brandOptions, setBrandOptions] = useState<string[]>([]);
|
||||
|
||||
@@ -189,7 +219,9 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
||||
<Combobox
|
||||
value={it.cat}
|
||||
onChange={(x) => patch(it.tempId, { cat: x.label })}
|
||||
items={materialOptions(materials, selectedTopic, it, "cat", items)}
|
||||
// پیشنهادها: همهی متریالهای کاتالوگ سراسری با نام واقعیشان
|
||||
// (بدون فیلتر تاپیک) — انتخابِ یک متریال موجود در ذخیره، همان را reuse میکند
|
||||
items={allMaterialNames}
|
||||
allowAdd
|
||||
addSuffix="به عنوان متریال جدید"
|
||||
placeholder="متریال"
|
||||
@@ -231,11 +263,31 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
||||
<Combobox
|
||||
value={it.unit}
|
||||
onChange={(x) => patch(it.tempId, { unit: x.label })}
|
||||
items={allUnits}
|
||||
// متنِ خودِ همین سطر از پیشنهادها حذف میشود؛ وگرنه متن تایپشده
|
||||
// «تطبیق دقیق» پیدا میکرد و گزینهی «به عنوان واحد جدید» هرگز ظاهر نمیشد
|
||||
items={allUnits.filter((u) => normalize(u) !== normalize(it.unit))}
|
||||
allowAdd
|
||||
addSuffix="اضافه کردن واحد جدید"
|
||||
addSuffix="به عنوان واحد جدید"
|
||||
placeholder="واحد"
|
||||
onInputChange={(v) => patch(it.tempId, { unit: v })}
|
||||
onAdd={(v) => {
|
||||
// انتخاب گزینهی «+ [واحد] به عنوان واحد جدید» → ثبت فوری در دیتابیس؛
|
||||
// نامِ تایپشده هم روی همان قلم اعمال میشود (onInputChange بالا)
|
||||
const name = v.trim();
|
||||
if (!name) return;
|
||||
void apiV26.catalog
|
||||
.createUnit(name)
|
||||
.then(() => {
|
||||
// بعد از ثبت، واحد برای سطرهای بعدی هم پیشنهاد شود
|
||||
setUnitOptions((prev) =>
|
||||
prev.some((u) => normalize(u) === normalize(name)) ? prev : [...prev, name],
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
// اگر واحدِ همنام در همین لحظه ساخته شده باشد (تداخل 409) یا شبکه
|
||||
// خطا دهد، هنگام ذخیرهی پیشنهاد دوباره resolve میشود — خطای بلوککننده نیست
|
||||
});
|
||||
}}
|
||||
inputClassName="!text-[12px] !h-9"
|
||||
className="min-w-0"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user