diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 85af404..3170a63 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1218,6 +1218,17 @@ export const api = { brandsList: async (): Promise<{ id: string; name: string }[]> => { return getData<{ id: string; name: string }[]>(`/brands?page_size=${PAGE_SIZE}`); }, + // مشخصه‌ها — رجیستری سراسری (attribute-definitions)، مستقل از تاپیک/دسته/برند. + // ویزارد از این لیست به‌عنوان منبع سراسری مشخصه‌ها استفاده می‌کند تا مشخصه‌های + // ثبت‌شده در پیشنهادهای قبلی همیشه در دسترس باشند. + specsList: async (): Promise => { + const raw = await getData<{ label?: string; attribute_key?: string }[]>( + `/attribute-definitions?page_size=${PAGE_SIZE}`, + ); + return raw + .map((d) => (d.label || "").trim() || (d.attribute_key || "").replace(/^spec_/, "").trim()) + .filter(Boolean); + }, // ---------- واحدها (از بک‌اند) ---------- unitsList: async (): Promise => { diff --git a/frontend/src/pages/Dashboard/EditModal.tsx b/frontend/src/pages/Dashboard/EditModal.tsx index c75dd71..a35ef2a 100644 --- a/frontend/src/pages/Dashboard/EditModal.tsx +++ b/frontend/src/pages/Dashboard/EditModal.tsx @@ -112,13 +112,24 @@ export function EditModal({ quoteId, onClose }: EditModalProps) { } }; - // گزینه‌های مشخصه — همه‌ی مشخصه‌های ثبت‌شده (کش) + // گزینه‌های مشخصه — همه‌ی مشخصه‌های ثبت‌شده (کش) + رجیستری سراسری + const [globalSpecs, setGlobalSpecs] = useState([]); + useEffect(() => { + let alive = true; + void api + .specsList() + .then((list) => alive && setGlobalSpecs(list)) + .catch(() => {}); + return () => { alive = false; }; + }, []); + const specOptions = useMemo(() => { const set = new Set(); + globalSpecs.forEach((s) => s && set.add(s)); materials.forEach((m) => (m.specs || []).forEach((s) => s && set.add(s))); itemRows.forEach((r) => r.specs.forEach((s) => s && set.add(s))); return [...set].sort((a, b) => a.localeCompare(b, "fa")); - }, [materials, itemRows]); + }, [materials, itemRows, globalSpecs]); // گزینه‌های پروژه — پروژه‌های ثبت‌شده + پروژه‌های خود پیشنهاد const projectOptions = useMemo(() => { diff --git a/frontend/src/pages/Wizard/Step2LineItems.tsx b/frontend/src/pages/Wizard/Step2LineItems.tsx index b737245..a408fe6 100644 --- a/frontend/src/pages/Wizard/Step2LineItems.tsx +++ b/frontend/src/pages/Wizard/Step2LineItems.tsx @@ -75,10 +75,10 @@ export function materialOptions( field: "cat" | "brand" | "spec", allItems: ItemDraft[] = [], apiBrands: string[] = [], + apiSpecs: string[] = [], ): string[] { let list = materials.filter((m) => topic && normalize(m.topic) === normalize(topic)); const cat = item.cat.trim(); - const brand = item.brand.trim(); const fromRows = (sel: (it: ItemDraft) => string | string[]): string[] => { const out = new Set(); @@ -96,15 +96,21 @@ export function materialOptions( fromRows((it) => it.cat).forEach((c) => base.add(c)); return [...base].sort((a, b) => a.localeCompare(b, "fa")); } - if (cat) list = list.filter((m) => normalize(m.category) === normalize(cat)); if (field === "brand") { + // برند سراسری است — مستقل از تاپیک و دسته const allBrands = new Set(materials.map((m) => m.brand).filter(Boolean)); apiBrands.forEach((b) => b && allBrands.add(b)); fromRows((it) => it.brand).forEach((b) => allBrands.add(b)); return [...allBrands].sort((a, b) => a.localeCompare(b, "fa")); } - if (brand) list = list.filter((m) => normalize(m.brand) === normalize(brand)); - const base = new Set(list.flatMap((m) => (m.specs || []).filter(Boolean))); + if (cat) list = list.filter((m) => normalize(m.category) === normalize(cat)); + // مشخصه‌ها سراسری هستند: رجیستری attribute-definitions + مشخصه‌های همه‌ی + // متریال‌ها + مشخصه‌های تایپ‌شده در سایر ردیف‌ها. به تاپیک/دسته/برندِ قلمِ + // فعلی گره نمی‌خورند تا هنگام افزودن پیشنهاد جدید همیشه در دسترس باشند. + const base = new Set(); + apiSpecs.forEach((s) => s && base.add(s)); + materials.forEach((m) => (m.specs || []).forEach((s) => s && base.add(s))); + list.forEach((m) => (m.specs || []).forEach((s) => s && base.add(s))); fromRows((it) => it.specs).forEach((s) => base.add(s)); return [...base].sort((a, b) => a.localeCompare(b, "fa")); } @@ -135,6 +141,7 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember }, [materials]); const [brandOptions, setBrandOptions] = useState([]); + const [specOptions, setSpecOptions] = useState([]); useEffect(() => { let alive = true; @@ -150,6 +157,10 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember .brandsList() .then((list) => alive && setBrandOptions(list.map((b) => b.name).filter(Boolean))) .catch(() => {}); + void api + .specsList() + .then((list) => alive && setSpecOptions(list)) + .catch(() => {}); return () => { alive = false; }; }, []); @@ -240,7 +251,7 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember patch(it.tempId, { specs })} - options={materialOptions(materials, selectedTopic, it, "spec", items)} + options={materialOptions(materials, selectedTopic, it, "spec", items, [], specOptions)} placeholder="مشخصه" className="min-w-0" />