feat(wizard): cache مشخصه globally instead of per topic/category/brand
The specs combobox drew only from materials in the selected topic, further
narrowed by the row's category and brand, so a spec was offered only when
some material in that topic shared the same category AND brand. On a new
quote the user therefore lost specs recorded in earlier quotes.
- api.specsList(): new global source reading the attribute-definitions
registry (topic/category/brand independent)
- materialOptions('spec'): unions the registry, specs of all materials,
and specs typed in sibling rows — no topic/category/brand filter
- EditModal: same global registry added to its spec options
Verified the registry endpoint returns labels and that the union includes
specs absent from material attributes (e.g. 'مسی').
This commit is contained in:
@@ -1218,6 +1218,17 @@ export const api = {
|
|||||||
brandsList: async (): Promise<{ id: string; name: string }[]> => {
|
brandsList: async (): Promise<{ id: string; name: string }[]> => {
|
||||||
return getData<{ id: string; name: string }[]>(`/brands?page_size=${PAGE_SIZE}`);
|
return getData<{ id: string; name: string }[]>(`/brands?page_size=${PAGE_SIZE}`);
|
||||||
},
|
},
|
||||||
|
// مشخصهها — رجیستری سراسری (attribute-definitions)، مستقل از تاپیک/دسته/برند.
|
||||||
|
// ویزارد از این لیست بهعنوان منبع سراسری مشخصهها استفاده میکند تا مشخصههای
|
||||||
|
// ثبتشده در پیشنهادهای قبلی همیشه در دسترس باشند.
|
||||||
|
specsList: async (): Promise<string[]> => {
|
||||||
|
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<RawUnit[]> => {
|
unitsList: async (): Promise<RawUnit[]> => {
|
||||||
|
|||||||
@@ -112,13 +112,24 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// گزینههای مشخصه — همهی مشخصههای ثبتشده (کش)
|
// گزینههای مشخصه — همهی مشخصههای ثبتشده (کش) + رجیستری سراسری
|
||||||
|
const [globalSpecs, setGlobalSpecs] = useState<string[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
void api
|
||||||
|
.specsList()
|
||||||
|
.then((list) => alive && setGlobalSpecs(list))
|
||||||
|
.catch(() => {});
|
||||||
|
return () => { alive = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
const specOptions = useMemo(() => {
|
const specOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
|
globalSpecs.forEach((s) => s && set.add(s));
|
||||||
materials.forEach((m) => (m.specs || []).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)));
|
itemRows.forEach((r) => r.specs.forEach((s) => s && set.add(s)));
|
||||||
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
||||||
}, [materials, itemRows]);
|
}, [materials, itemRows, globalSpecs]);
|
||||||
|
|
||||||
// گزینههای پروژه — پروژههای ثبتشده + پروژههای خود پیشنهاد
|
// گزینههای پروژه — پروژههای ثبتشده + پروژههای خود پیشنهاد
|
||||||
const projectOptions = useMemo(() => {
|
const projectOptions = useMemo(() => {
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ export function materialOptions(
|
|||||||
field: "cat" | "brand" | "spec",
|
field: "cat" | "brand" | "spec",
|
||||||
allItems: ItemDraft[] = [],
|
allItems: ItemDraft[] = [],
|
||||||
apiBrands: string[] = [],
|
apiBrands: string[] = [],
|
||||||
|
apiSpecs: string[] = [],
|
||||||
): string[] {
|
): string[] {
|
||||||
let list = materials.filter((m) => topic && normalize(m.topic) === normalize(topic));
|
let list = materials.filter((m) => topic && normalize(m.topic) === normalize(topic));
|
||||||
const cat = item.cat.trim();
|
const cat = item.cat.trim();
|
||||||
const brand = item.brand.trim();
|
|
||||||
|
|
||||||
const fromRows = (sel: (it: ItemDraft) => string | string[]): string[] => {
|
const fromRows = (sel: (it: ItemDraft) => string | string[]): string[] => {
|
||||||
const out = new Set<string>();
|
const out = new Set<string>();
|
||||||
@@ -96,15 +96,21 @@ export function materialOptions(
|
|||||||
fromRows((it) => it.cat).forEach((c) => base.add(c));
|
fromRows((it) => it.cat).forEach((c) => base.add(c));
|
||||||
return [...base].sort((a, b) => a.localeCompare(b, "fa"));
|
return [...base].sort((a, b) => a.localeCompare(b, "fa"));
|
||||||
}
|
}
|
||||||
if (cat) list = list.filter((m) => normalize(m.category) === normalize(cat));
|
|
||||||
if (field === "brand") {
|
if (field === "brand") {
|
||||||
|
// برند سراسری است — مستقل از تاپیک و دسته
|
||||||
const allBrands = new Set(materials.map((m) => m.brand).filter(Boolean));
|
const allBrands = new Set(materials.map((m) => m.brand).filter(Boolean));
|
||||||
apiBrands.forEach((b) => b && allBrands.add(b));
|
apiBrands.forEach((b) => b && allBrands.add(b));
|
||||||
fromRows((it) => it.brand).forEach((b) => allBrands.add(b));
|
fromRows((it) => it.brand).forEach((b) => allBrands.add(b));
|
||||||
return [...allBrands].sort((a, b) => a.localeCompare(b, "fa"));
|
return [...allBrands].sort((a, b) => a.localeCompare(b, "fa"));
|
||||||
}
|
}
|
||||||
if (brand) list = list.filter((m) => normalize(m.brand) === normalize(brand));
|
if (cat) list = list.filter((m) => normalize(m.category) === normalize(cat));
|
||||||
const base = new Set(list.flatMap((m) => (m.specs || []).filter(Boolean)));
|
// مشخصهها سراسری هستند: رجیستری attribute-definitions + مشخصههای همهی
|
||||||
|
// متریالها + مشخصههای تایپشده در سایر ردیفها. به تاپیک/دسته/برندِ قلمِ
|
||||||
|
// فعلی گره نمیخورند تا هنگام افزودن پیشنهاد جدید همیشه در دسترس باشند.
|
||||||
|
const base = new Set<string>();
|
||||||
|
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));
|
fromRows((it) => it.specs).forEach((s) => base.add(s));
|
||||||
return [...base].sort((a, b) => a.localeCompare(b, "fa"));
|
return [...base].sort((a, b) => a.localeCompare(b, "fa"));
|
||||||
}
|
}
|
||||||
@@ -135,6 +141,7 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
|||||||
}, [materials]);
|
}, [materials]);
|
||||||
|
|
||||||
const [brandOptions, setBrandOptions] = useState<string[]>([]);
|
const [brandOptions, setBrandOptions] = useState<string[]>([]);
|
||||||
|
const [specOptions, setSpecOptions] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -150,6 +157,10 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
|||||||
.brandsList()
|
.brandsList()
|
||||||
.then((list) => alive && setBrandOptions(list.map((b) => b.name).filter(Boolean)))
|
.then((list) => alive && setBrandOptions(list.map((b) => b.name).filter(Boolean)))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
void api
|
||||||
|
.specsList()
|
||||||
|
.then((list) => alive && setSpecOptions(list))
|
||||||
|
.catch(() => {});
|
||||||
return () => { alive = false; };
|
return () => { alive = false; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -240,7 +251,7 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
|||||||
<MultiSelectCombobox
|
<MultiSelectCombobox
|
||||||
values={it.specs}
|
values={it.specs}
|
||||||
onChange={(specs) => patch(it.tempId, { specs })}
|
onChange={(specs) => patch(it.tempId, { specs })}
|
||||||
options={materialOptions(materials, selectedTopic, it, "spec", items)}
|
options={materialOptions(materials, selectedTopic, it, "spec", items, [], specOptions)}
|
||||||
placeholder="مشخصه"
|
placeholder="مشخصه"
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user