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 { Combobox } from "../../components/Combobox";
|
||||||
import { NumberInput } from "../../components/NumberInput";
|
import { NumberInput } from "../../components/NumberInput";
|
||||||
import { MultiSelectCombobox } from "../../components/MultiSelectCombobox";
|
import { MultiSelectCombobox } from "../../components/MultiSelectCombobox";
|
||||||
import { api } from "../../lib/api";
|
import { api, apiV26 } from "../../lib/api";
|
||||||
|
|
||||||
export interface ItemDraft {
|
export interface ItemDraft {
|
||||||
tempId: string;
|
tempId: string;
|
||||||
@@ -41,6 +41,25 @@ export function itemRowLabel(it: ItemDraft): string {
|
|||||||
return qtyPart ? `${name} -- ${qtyPart}` : name;
|
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 داخل تاپیک — با فیلتر پلهبهپله (تاپیک → دسته → برند)
|
// مقادیر مجزای category/brand/spec داخل تاپیک — با فیلتر پلهبهپله (تاپیک → دسته → برند)
|
||||||
// + مقادیر تایپشده در همهی سطرهای قبلی (suggestion از همهی فیلدها)
|
// + مقادیر تایپشده در همهی سطرهای قبلی (suggestion از همهی فیلدها)
|
||||||
// نکته: برند و واحد از همهی دستهها نمایش داده میشوند (issue #7)
|
// نکته: برند و واحد از همهی دستهها نمایش داده میشوند (issue #7)
|
||||||
@@ -97,13 +116,24 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
|||||||
items.forEach((it) => { if (it.unit) all.add(it.unit); });
|
items.forEach((it) => { if (it.unit) all.add(it.unit); });
|
||||||
return [...all];
|
return [...all];
|
||||||
});
|
});
|
||||||
// واحدهای تایپشده در سطرهای قبلی
|
// واحدهای موجود (دیتابیس) + واحدهای تایپشده در سطرهای دیگر
|
||||||
const allUnits = useMemo(() => {
|
const allUnits = useMemo(() => {
|
||||||
const out = new Set(unitOptions);
|
const out = new Set(unitOptions);
|
||||||
items.forEach((it) => { if (it.unit) out.add(it.unit); });
|
items.forEach((it) => { if (it.unit) out.add(it.unit); });
|
||||||
return [...out];
|
return [...out];
|
||||||
}, [items, unitOptions]);
|
}, [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[]>([]);
|
const [brandOptions, setBrandOptions] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -189,7 +219,9 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
|||||||
<Combobox
|
<Combobox
|
||||||
value={it.cat}
|
value={it.cat}
|
||||||
onChange={(x) => patch(it.tempId, { cat: x.label })}
|
onChange={(x) => patch(it.tempId, { cat: x.label })}
|
||||||
items={materialOptions(materials, selectedTopic, it, "cat", items)}
|
// پیشنهادها: همهی متریالهای کاتالوگ سراسری با نام واقعیشان
|
||||||
|
// (بدون فیلتر تاپیک) — انتخابِ یک متریال موجود در ذخیره، همان را reuse میکند
|
||||||
|
items={allMaterialNames}
|
||||||
allowAdd
|
allowAdd
|
||||||
addSuffix="به عنوان متریال جدید"
|
addSuffix="به عنوان متریال جدید"
|
||||||
placeholder="متریال"
|
placeholder="متریال"
|
||||||
@@ -231,11 +263,31 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
|||||||
<Combobox
|
<Combobox
|
||||||
value={it.unit}
|
value={it.unit}
|
||||||
onChange={(x) => patch(it.tempId, { unit: x.label })}
|
onChange={(x) => patch(it.tempId, { unit: x.label })}
|
||||||
items={allUnits}
|
// متنِ خودِ همین سطر از پیشنهادها حذف میشود؛ وگرنه متن تایپشده
|
||||||
|
// «تطبیق دقیق» پیدا میکرد و گزینهی «به عنوان واحد جدید» هرگز ظاهر نمیشد
|
||||||
|
items={allUnits.filter((u) => normalize(u) !== normalize(it.unit))}
|
||||||
allowAdd
|
allowAdd
|
||||||
addSuffix="اضافه کردن واحد جدید"
|
addSuffix="به عنوان واحد جدید"
|
||||||
placeholder="واحد"
|
placeholder="واحد"
|
||||||
onInputChange={(v) => patch(it.tempId, { unit: v })}
|
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"
|
inputClassName="!text-[12px] !h-9"
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -256,12 +256,10 @@ export function WizardPage() {
|
|||||||
const total = it.total;
|
const total = it.total;
|
||||||
if (!cat && !brand && qty == null && price == null) continue;
|
if (!cat && !brand && qty == null && price == null) continue;
|
||||||
|
|
||||||
// جستجوی متریال: اول با نام (مثل بکاند ساخته شده) — بعد از رفرش،
|
// جستجوی متریال: اول با نامِ کامل سراسری (دیتابیس) — بدون قید دسته/تاپیک،
|
||||||
// برند/مشخصه در حافظه نیستند ولی نام کامل در دیتابیس ذخیره شده
|
// چون دستهی ذخیرهشدهی متریالها نام تاپیک است نه نام تایپشدهی کاربر
|
||||||
const wantName = [brand, cat, ...specs].filter(Boolean).join(" ") || "کالای جدید";
|
const wantName = [brand, cat, ...specs].filter(Boolean).join(" ") || "کالای جدید";
|
||||||
let material = materials.find(
|
let material = materials.find((m) => normalize(m.name) === normalize(wantName));
|
||||||
(m) => normalize(m.category) === normalize(cat) && normalize(m.name) === normalize(wantName),
|
|
||||||
);
|
|
||||||
if (!material) {
|
if (!material) {
|
||||||
// جستجوی قبلی (برند و مشخصهی ساختارمند در همین نشست)
|
// جستجوی قبلی (برند و مشخصهی ساختارمند در همین نشست)
|
||||||
material = materials.find(
|
material = materials.find(
|
||||||
|
|||||||
Reference in New Issue
Block a user