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;