diff --git a/app/components/catalog/CatalogConfigurator.vue b/app/components/catalog/CatalogConfigurator.vue index d87dcdb..48e946e 100644 --- a/app/components/catalog/CatalogConfigurator.vue +++ b/app/components/catalog/CatalogConfigurator.vue @@ -4,19 +4,20 @@ import { ClientProductsDocument, type ClientProductsQuery } from '~/composables/ import { useClientCart } from '~/composables/useClientCart'; type ProductNode = ClientProductsQuery['clientProducts'][number]; -type ParamFieldKey = 'widthMm' | 'lengthM' | 'thicknessMicron' | 'sleeveBrand' | 'quantityPerBox'; +type ParamFieldKey = 'widthMm' | 'lengthM' | 'thicknessMicron' | 'sleeveBrand' | 'quantityPerBox' | 'colorTag' | 'labelTag'; type ParamValue = number | string; type ParsedProduct = ProductNode & { productTypeLabel: string; quantityPerBoxOptions: string[]; normalizedTags: string[]; + colorTags: string[]; + labelTags: string[]; }; type ProductGroup = { key: string; typeLabel: string; - tags: string[]; products: ParsedProduct[]; }; @@ -26,16 +27,22 @@ type GroupState = { thicknessMicron: number | null; sleeveBrand: string | null; quantityPerBox: string | null; + colorTag: string | null; + labelTag: string | null; isExpanded: boolean; }; -const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand', 'quantityPerBox']; +const COLOR_TAGS = ['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый']; +const LABEL_TAGS = ['хрупкое', 'подарок', 'акция']; +const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand', 'quantityPerBox', 'colorTag', 'labelTag']; const parameterFields: Array<{ key: ParamFieldKey; label: string }> = [ { key: 'widthMm', label: 'Ширина' }, { key: 'lengthM', label: 'Длина' }, { key: 'thicknessMicron', label: 'Толщина' }, { key: 'sleeveBrand', label: 'Втулка' }, { key: 'quantityPerBox', label: 'Короб' }, + { key: 'colorTag', label: 'Цвет' }, + { key: 'labelTag', label: 'Надпись' }, ]; const coverPresets = [ @@ -87,11 +94,15 @@ function createProductCover(name: string, sku: string) { } function hydrateProduct(product: ProductNode): ParsedProduct { + const normalizedTags = product.tags.map((tag) => normalizeText(tag)).filter(Boolean).sort((a, b) => a.localeCompare(b, 'ru')); + return { ...product, productTypeLabel: normalizeText(product.productType) || 'Без типа', quantityPerBoxOptions: splitBoxValues(product.quantityPerBox), - normalizedTags: product.tags.map((tag) => normalizeText(tag)).filter(Boolean).sort((a, b) => a.localeCompare(b, 'ru')), + normalizedTags, + colorTags: normalizedTags.filter((tag) => COLOR_TAGS.includes(tag)), + labelTags: normalizedTags.filter((tag) => LABEL_TAGS.includes(tag)), }; } @@ -153,43 +164,25 @@ const productGroups = computed(() => { const map = new Map(); for (const product of parsedProducts.value) { - const tagsKey = product.normalizedTags.join('|'); - const groupKey = `${product.productTypeLabel}::${tagsKey}`; - const existing = map.get(groupKey); + const existing = map.get(product.productTypeLabel); if (existing) { existing.push(product); } else { - map.set(groupKey, [product]); + map.set(product.productTypeLabel, [product]); } } return [...map.entries()] - .sort((a, b) => { - const firstProduct = a[1][0]; - const secondProduct = b[1][0]; - const byType = firstProduct.productTypeLabel.localeCompare(secondProduct.productTypeLabel, 'ru'); - if (byType !== 0) { - return byType; - } - - return firstProduct.normalizedTags.join('|').localeCompare(secondProduct.normalizedTags.join('|'), 'ru'); - }) - .map(([groupSignature, products]) => { - const firstProduct = products[0]; - const key = groupSignature + .sort((a, b) => a[0].localeCompare(b[0], 'ru')) + .map(([typeLabel, products]) => ({ + key: typeLabel .toLowerCase() - .replaceAll(/[^a-z0-9а-яё|]+/gi, '-') - .replaceAll('|', '--') + .replaceAll(/[^a-z0-9а-яё]+/gi, '-') .replaceAll(/-+/g, '-') - .replaceAll(/^-|-$/g, ''); - - return { - key, - typeLabel: firstProduct.productTypeLabel, - tags: firstProduct.normalizedTags, + .replaceAll(/^-|-$/g, ''), + typeLabel, products: [...products].sort(compareProducts), - }; - }); + })); }); function sortParamValues(values: ParamValue[]) { @@ -212,6 +205,20 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) { continue; } + if (field === 'colorTag') { + for (const tag of product.colorTags) { + values.add(tag); + } + continue; + } + + if (field === 'labelTag') { + for (const tag of product.labelTags) { + values.add(tag); + } + continue; + } + const value = product[field]; if (value !== null && value !== undefined) { values.add(value); @@ -235,7 +242,7 @@ function visibleFieldsByColumn(group: ProductGroup) { const rightColumn = parameterFields.filter((field) => ( visibleKeys.has(field.key) - && ['thicknessMicron', 'quantityPerBox', 'sleeveBrand'].includes(field.key) + && ['thicknessMicron', 'quantityPerBox', 'sleeveBrand', 'colorTag', 'labelTag'].includes(field.key) )); return { leftColumn, rightColumn }; @@ -254,6 +261,8 @@ function createGroupState(group: ProductGroup): GroupState { thicknessMicron: firstProduct?.thicknessMicron ?? null, sleeveBrand: firstProduct?.sleeveBrand ?? null, quantityPerBox: firstProduct?.quantityPerBoxOptions[0] ?? null, + colorTag: firstProduct?.colorTags[0] ?? null, + labelTag: firstProduct?.labelTags[0] ?? null, isExpanded: false, }; } @@ -291,6 +300,14 @@ function matchesProductState(product: ParsedProduct, state: GroupState, keys: Pa return product.quantityPerBoxOptions.includes(String(state[key])); } + if (key === 'colorTag') { + return product.colorTags.includes(String(state[key])); + } + + if (key === 'labelTag') { + return product.labelTags.includes(String(state[key])); + } + return product[key] === state[key]; }); } @@ -329,6 +346,14 @@ function productHasOption(product: ParsedProduct, field: ParamFieldKey, option: return product.quantityPerBoxOptions.includes(String(option)); } + if (field === 'colorTag') { + return product.colorTags.includes(String(option)); + } + + if (field === 'labelTag') { + return product.labelTags.includes(String(option)); + } + return product[field] === option; } @@ -385,6 +410,8 @@ function applyProductToState(state: GroupState, product: ParsedProduct, preferre state.lengthM = product.lengthM ?? null; state.thicknessMicron = product.thicknessMicron ?? null; state.sleeveBrand = product.sleeveBrand ?? null; + state.colorTag = product.colorTags[0] ?? null; + state.labelTag = product.labelTags[0] ?? null; if (preferredBoxOption !== null && product.quantityPerBoxOptions.includes(String(preferredBoxOption))) { state.quantityPerBox = String(preferredBoxOption); @@ -487,16 +514,6 @@ function decrementSelected(group: ProductGroup) {

{{ group.typeLabel }}

- -
- - {{ tag }} - -