fix(wizard): store material name cleanly instead of brand+category+specs
Adding a new item wrote the material name as 'brand + category + specs' (e.g. 'پارس لوله لوله PE100 سایز ۱۶'), while specs were also stored correctly in EAV attributes. The wizard then tried to recover the base name by stripping brand and specs out of that string, but never stripped the injected category — so the spec stayed baked into the cached material name and the مشخصه appeared as part of the material name on later visits. - api.ts / WizardPage / EditModal: build name from the material itself only (brand goes to brand_ids, specs to attributes) - materialBaseName: strip category too, never strip a part that would empty the name, and reject a spec-only result; legacy rows whose name cannot be disentangled fall back to the stored name rather than a guess - WizardPage: match existing materials on category+brand so legacy rows are still reused instead of duplicated
This commit is contained in:
@@ -1041,8 +1041,10 @@ export const api = {
|
|||||||
const unitId = await unitIdByNameOrCreate(lookup, unitName);
|
const unitId = await unitIdByNameOrCreate(lookup, unitName);
|
||||||
// برند — اگر تعریف نشده، در جدول brands ساخته و به متریال متصل میشود
|
// برند — اگر تعریف نشده، در جدول brands ساخته و به متریال متصل میشود
|
||||||
const brandId = await brandIdByNameOrCreate(lookup, m.brand);
|
const brandId = await brandIdByNameOrCreate(lookup, m.brand);
|
||||||
// تایپ جلوی برنامه name ندارد — از متریال/برند/مشخصه ساخته میشود
|
// name فقط نامِ خودِ متریال است؛ برند در brand_ids و مشخصهها در
|
||||||
const name = [m.brand, m.category, ...(m.specs || [])].filter(Boolean).join(" ") || "کالای جدید";
|
// attributes (EAV) ذخیره میشوند. اگر برند/مشخصه داخل name ریخته شود،
|
||||||
|
// در ویزارد بهعنوان «نامِ متریال» کش میشود و مشخصهها در نام ظاهر میشوند.
|
||||||
|
const name = (m.category || m.brand || (m.specs || []).join(" ") || "").trim() || "کالای جدید";
|
||||||
// مشخصهها بهصورت attribute (EAV) ذخیره میشوند تا بعد از رفرش دیده شوند
|
// مشخصهها بهصورت attribute (EAV) ذخیره میشوند تا بعد از رفرش دیده شوند
|
||||||
const attrs: { attribute_definition_id: string; value: string }[] = [];
|
const attrs: { attribute_definition_id: string; value: string }[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
|||||||
} else {
|
} else {
|
||||||
const nm = {
|
const nm = {
|
||||||
id: nextId("m", materialsCopy),
|
id: nextId("m", materialsCopy),
|
||||||
name: [brand, cat, ...specs].filter(Boolean).join(" ") || "کالای جدید",
|
name: (cat || brand || specs.join(" ") || "").trim() || "کالای جدید",
|
||||||
topic: materialById(materials, row.origLi?.material_id || "").topic || "",
|
topic: materialById(materials, row.origLi?.material_id || "").topic || "",
|
||||||
category: cat,
|
category: cat,
|
||||||
brand,
|
brand,
|
||||||
|
|||||||
@@ -41,21 +41,30 @@ 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 {
|
// متریالهای جدید فقط نامِ خودِ متریال را در name دارند (برند در brand_ids و
|
||||||
let base = (name || "").trim();
|
// مشخصهها در attributes ذخیره میشوند) و دستنخورده برمیگردند. رکوردهای قدیمی
|
||||||
if (!base) return "";
|
// name را به شکل «برند + دسته + مشخصه» ذخیره کردهاند؛ آن قطعات جدا میشوند تا
|
||||||
if (brand) {
|
// نامِ پایه باقی بماند.
|
||||||
if (base.startsWith(brand)) base = base.slice(brand.length);
|
function materialBaseName(name: string, brand: string, specs: string[], category = ""): string {
|
||||||
else if (base.includes(brand)) base = base.split(brand).join(" ");
|
const fallback = (name || "").trim();
|
||||||
|
if (!fallback) return "";
|
||||||
|
let base = fallback;
|
||||||
|
// بلندترین قطعه اول حذف میشود تا حذفهای جزئی، رشته را ناقص نکنند
|
||||||
|
const parts = [brand, category, ...specs]
|
||||||
|
.map((p) => (p || "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => b.length - a.length);
|
||||||
|
for (const p of parts) {
|
||||||
|
if (!base.includes(p)) continue;
|
||||||
|
const next = base.split(p).join(" ").replace(/\s+/g, " ").trim();
|
||||||
|
// اگر حذفِ این قطعه کلِ نام را خالی کند، یعنی همان نامِ متریال بوده — نگهش دار
|
||||||
|
if (!next) continue;
|
||||||
|
base = next;
|
||||||
}
|
}
|
||||||
specs
|
// شکلِ قدیمیِ خراب میتواند فقط یک مشخصه باقی بگذارد؛ آن دیگر نامِ متریال نیست
|
||||||
.filter((s) => s && s.trim())
|
if (parts.some((p) => p === base)) return fallback;
|
||||||
.forEach((s) => {
|
return base || fallback;
|
||||||
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 داخل تاپیک
|
||||||
@@ -119,7 +128,7 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
|||||||
const allMaterialNames = useMemo(() => {
|
const allMaterialNames = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
materials.forEach((m) => {
|
materials.forEach((m) => {
|
||||||
const base = materialBaseName(m.name, m.brand, m.specs);
|
const base = materialBaseName(m.name, m.brand, m.specs, m.category);
|
||||||
if (base) set.add(base);
|
if (base) set.add(base);
|
||||||
});
|
});
|
||||||
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function WizardPage() {
|
|||||||
const rememberMaterial = (rec: MaterialRec) => {
|
const rememberMaterial = (rec: MaterialRec) => {
|
||||||
void addMaterial.mutateAsync({
|
void addMaterial.mutateAsync({
|
||||||
id: nextId("m", materials),
|
id: nextId("m", materials),
|
||||||
name: [rec.brand, rec.category, ...rec.specs].filter(Boolean).join(" ") || "کالای جدید",
|
name: (rec.category || rec.brand || rec.specs.join(" ") || "").trim() || "کالای جدید",
|
||||||
topic: rec.topic,
|
topic: rec.topic,
|
||||||
category: rec.category,
|
category: rec.category,
|
||||||
brand: rec.brand,
|
brand: rec.brand,
|
||||||
@@ -243,7 +243,7 @@ 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 = (cat || brand || specs.join(" ") || "").trim() || "کالای جدید";
|
||||||
let material = materials.find((m) => normalize(m.name) === normalize(wantName));
|
let material = materials.find((m) => normalize(m.name) === normalize(wantName));
|
||||||
if (!material) {
|
if (!material) {
|
||||||
material = materials.find(
|
material = materials.find(
|
||||||
@@ -253,6 +253,13 @@ export function WizardPage() {
|
|||||||
specsKey(m.specs) === specsKey(specs),
|
specsKey(m.specs) === specsKey(specs),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// رکوردهای قدیمی name را به شکل «برند + دسته + مشخصه» ذخیره کردهاند؛
|
||||||
|
// تطبیق دوم بر اساس category/brand/specs انجام میشود تا آنها هم پیدا شوند.
|
||||||
|
if (!material && cat) {
|
||||||
|
material = materials.find(
|
||||||
|
(m) => normalize(m.category) === normalize(cat) && normalize(m.brand) === normalize(brand),
|
||||||
|
);
|
||||||
|
}
|
||||||
let materialId: string;
|
let materialId: string;
|
||||||
if (material) {
|
if (material) {
|
||||||
materialId = material.id;
|
materialId = material.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user