From ae977e5e66e09c2e941a887784201a209a02ef33 Mon Sep 17 00:00:00 2001 From: MMD Date: Wed, 16 Sep 2026 20:24:45 +0330 Subject: [PATCH] fix(wizard): store material name cleanly instead of brand+category+specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/lib/api.ts | 6 ++- frontend/src/pages/Dashboard/EditModal.tsx | 2 +- frontend/src/pages/Wizard/Step2LineItems.tsx | 39 ++++++++++++-------- frontend/src/pages/Wizard/WizardPage.tsx | 11 +++++- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index e9e6718..85af404 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1041,8 +1041,10 @@ export const api = { const unitId = await unitIdByNameOrCreate(lookup, unitName); // برند — اگر تعریف نشده، در جدول brands ساخته و به متریال متصل می‌شود const brandId = await brandIdByNameOrCreate(lookup, m.brand); - // تایپ جلوی برنامه name ندارد — از متریال/برند/مشخصه ساخته می‌شود - const name = [m.brand, m.category, ...(m.specs || [])].filter(Boolean).join(" ") || "کالای جدید"; + // name فقط نامِ خودِ متریال است؛ برند در brand_ids و مشخصه‌ها در + // attributes (EAV) ذخیره می‌شوند. اگر برند/مشخصه داخل name ریخته شود، + // در ویزارد به‌عنوان «نامِ متریال» کش می‌شود و مشخصه‌ها در نام ظاهر می‌شوند. + const name = (m.category || m.brand || (m.specs || []).join(" ") || "").trim() || "کالای جدید"; // مشخصه‌ها به‌صورت attribute (EAV) ذخیره می‌شوند تا بعد از رفرش دیده شوند const attrs: { attribute_definition_id: string; value: string }[] = []; const seen = new Set(); diff --git a/frontend/src/pages/Dashboard/EditModal.tsx b/frontend/src/pages/Dashboard/EditModal.tsx index 2945d23..c75dd71 100644 --- a/frontend/src/pages/Dashboard/EditModal.tsx +++ b/frontend/src/pages/Dashboard/EditModal.tsx @@ -299,7 +299,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) { } else { const nm = { 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 || "", category: cat, brand, diff --git a/frontend/src/pages/Wizard/Step2LineItems.tsx b/frontend/src/pages/Wizard/Step2LineItems.tsx index 95b3761..b737245 100644 --- a/frontend/src/pages/Wizard/Step2LineItems.tsx +++ b/frontend/src/pages/Wizard/Step2LineItems.tsx @@ -41,21 +41,30 @@ 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(" "); +// نام پایه‌ی متریال از نامِ ذخیره‌شده. +// متریال‌های جدید فقط نامِ خودِ متریال را در name دارند (برند در brand_ids و +// مشخصه‌ها در attributes ذخیره می‌شوند) و دست‌نخورده برمی‌گردند. رکوردهای قدیمی +// name را به شکل «برند + دسته + مشخصه» ذخیره کرده‌اند؛ آن قطعات جدا می‌شوند تا +// نامِ پایه باقی بماند. +function materialBaseName(name: string, brand: string, specs: string[], category = ""): string { + 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()) - .forEach((s) => { - if (base.includes(s)) base = base.split(s).join(" "); - }); - base = base.replace(/\s+/g, " ").trim(); - return base || (name || "").trim(); + // شکلِ قدیمیِ خراب می‌تواند فقط یک مشخصه باقی بگذارد؛ آن دیگر نامِ متریال نیست + if (parts.some((p) => p === base)) return fallback; + return base || fallback; } // مقادیر مجزای category/brand/spec داخل تاپیک @@ -119,7 +128,7 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember const allMaterialNames = useMemo(() => { const set = new Set(); 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); }); return [...set].sort((a, b) => a.localeCompare(b, "fa")); diff --git a/frontend/src/pages/Wizard/WizardPage.tsx b/frontend/src/pages/Wizard/WizardPage.tsx index b6fc451..1dce5d2 100644 --- a/frontend/src/pages/Wizard/WizardPage.tsx +++ b/frontend/src/pages/Wizard/WizardPage.tsx @@ -128,7 +128,7 @@ export function WizardPage() { const rememberMaterial = (rec: MaterialRec) => { void addMaterial.mutateAsync({ 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, category: rec.category, brand: rec.brand, @@ -243,7 +243,7 @@ export function WizardPage() { const total = it.total; 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)); if (!material) { material = materials.find( @@ -253,6 +253,13 @@ export function WizardPage() { 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; if (material) { materialId = material.id;