From 6f1df4bf0001934c719b8d50a657e13a35813f77 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:10:53 +0700 Subject: [PATCH] Make catalog settings editable --- app/composables/graphql/generated.ts | 28 +- app/pages/catalog-settings.vue | 247 ++++++++++++------ .../catalog-product-type-settings.graphql | 6 + ...psert-catalog-product-type-setting.graphql | 6 + graphql/schema.graphql | 12 + 5 files changed, 213 insertions(+), 86 deletions(-) diff --git a/app/composables/graphql/generated.ts b/app/composables/graphql/generated.ts index f8a6d79..6c6d1f1 100644 --- a/app/composables/graphql/generated.ts +++ b/app/composables/graphql/generated.ts @@ -94,11 +94,17 @@ export type CatalogProductTypeSetting = { allowCustomLabel: Scalars['Boolean']['output']; allowCustomLength: Scalars['Boolean']['output']; allowCustomSleeveBrand: Scalars['Boolean']['output']; + colorOptions: Array; customLengthMaxM?: Maybe; customLengthMinM?: Maybe; customLengthStepM?: Maybe; + labelOptions: Array; + lengthOptionsM: Array; productType: Scalars['String']['output']; showQuantityPerBox: Scalars['Boolean']['output']; + sleeveOptions: Array; + thicknessOptionsMicron: Array; + widthOptionsMm: Array; }; export type Company = { @@ -786,11 +792,17 @@ export type UpsertCatalogProductTypeSettingInput = { allowCustomLabel: Scalars['Boolean']['input']; allowCustomLength: Scalars['Boolean']['input']; allowCustomSleeveBrand: Scalars['Boolean']['input']; + colorOptions: Array; customLengthMaxM?: InputMaybe; customLengthMinM?: InputMaybe; customLengthStepM?: InputMaybe; + labelOptions: Array; + lengthOptionsM: Array; productType: Scalars['String']['input']; showQuantityPerBox: Scalars['Boolean']['input']; + sleeveOptions: Array; + thicknessOptionsMicron: Array; + widthOptionsMm: Array; }; export type UpsertMyCounterpartyProfileInput = { @@ -1158,7 +1170,7 @@ export type UpsertMyCounterpartyProfileMutation = { __typename?: 'Mutation', ups export type CatalogProductTypeSettingsQueryVariables = Exact<{ [key: string]: never; }>; -export type CatalogProductTypeSettingsQuery = { __typename?: 'Query', catalogProductTypeSettings: Array<{ __typename?: 'CatalogProductTypeSetting', productType: string, showQuantityPerBox: boolean, allowCustomLength: boolean, customLengthMinM?: number | null, customLengthMaxM?: number | null, customLengthStepM?: number | null, allowCustomSleeveBrand: boolean, allowCustomLabel: boolean }> }; +export type CatalogProductTypeSettingsQuery = { __typename?: 'Query', catalogProductTypeSettings: Array<{ __typename?: 'CatalogProductTypeSetting', productType: string, showQuantityPerBox: boolean, allowCustomLength: boolean, customLengthMinM?: number | null, customLengthMaxM?: number | null, customLengthStepM?: number | null, allowCustomSleeveBrand: boolean, allowCustomLabel: boolean, widthOptionsMm: Array, lengthOptionsM: Array, thicknessOptionsMicron: Array, sleeveOptions: Array, colorOptions: Array, labelOptions: Array }> }; export type IntegrationSyncDashboardQueryVariables = Exact<{ [key: string]: never; }>; @@ -1170,7 +1182,7 @@ export type UpsertCatalogProductTypeSettingMutationVariables = Exact<{ }>; -export type UpsertCatalogProductTypeSettingMutation = { __typename?: 'Mutation', upsertCatalogProductTypeSetting: { __typename?: 'CatalogProductTypeSetting', productType: string, showQuantityPerBox: boolean, allowCustomLength: boolean, customLengthMinM?: number | null, customLengthMaxM?: number | null, customLengthStepM?: number | null, allowCustomSleeveBrand: boolean, allowCustomLabel: boolean } }; +export type UpsertCatalogProductTypeSettingMutation = { __typename?: 'Mutation', upsertCatalogProductTypeSetting: { __typename?: 'CatalogProductTypeSetting', productType: string, showQuantityPerBox: boolean, allowCustomLength: boolean, customLengthMinM?: number | null, customLengthMaxM?: number | null, customLengthStepM?: number | null, allowCustomSleeveBrand: boolean, allowCustomLabel: boolean, widthOptionsMm: Array, lengthOptionsM: Array, thicknessOptionsMicron: Array, sleeveOptions: Array, colorOptions: Array, labelOptions: Array } }; export const ConsumeLoginTokenDocument = gql` @@ -2980,6 +2992,12 @@ export const CatalogProductTypeSettingsDocument = gql` customLengthStepM allowCustomSleeveBrand allowCustomLabel + widthOptionsMm + lengthOptionsM + thicknessOptionsMicron + sleeveOptions + colorOptions + labelOptions } } `; @@ -3055,6 +3073,12 @@ export const UpsertCatalogProductTypeSettingDocument = gql` customLengthStepM allowCustomSleeveBrand allowCustomLabel + widthOptionsMm + lengthOptionsM + thicknessOptionsMicron + sleeveOptions + colorOptions + labelOptions } } `; diff --git a/app/pages/catalog-settings.vue b/app/pages/catalog-settings.vue index 03efdc8..be9f253 100644 --- a/app/pages/catalog-settings.vue +++ b/app/pages/catalog-settings.vue @@ -2,10 +2,8 @@ import { useMutation, useQuery } from '@vue/apollo-composable'; import { CatalogProductTypeSettingsDocument, - ClientProductsDocument, UpsertCatalogProductTypeSettingDocument, type CatalogProductTypeSettingsQuery, - type ClientProductsQuery, } from '~/composables/graphql/generated'; definePageMeta({ @@ -14,7 +12,14 @@ definePageMeta({ }); type CatalogSettingItem = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number]; -type ProductNode = ClientProductsQuery['clientProducts'][number]; +type OptionKey = + | 'widthOptionsMm' + | 'lengthOptionsM' + | 'thicknessOptionsMicron' + | 'sleeveOptions' + | 'colorOptions' + | 'labelOptions'; +type OptionKind = 'number' | 'text'; type CatalogSettingForm = { productType: string; allowCustomLength: boolean; @@ -23,17 +28,32 @@ type CatalogSettingForm = { customLengthStepM: string; allowCustomSleeveBrand: boolean; allowCustomLabel: boolean; + widthOptionsMm: string[]; + lengthOptionsM: string[]; + thicknessOptionsMicron: string[]; + sleeveOptions: string[]; + colorOptions: string[]; + labelOptions: string[]; + drafts: Record; }; -type StandardOptionGroup = { +type OptionGroupDefinition = { + key: OptionKey; label: string; - values: string[]; + placeholder: string; + kind: OptionKind; + suffix?: string; }; -const COLOR_TAGS = ['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый']; -const LABEL_TAGS = ['хрупкое', 'подарок', 'акция']; +const OPTION_GROUPS: OptionGroupDefinition[] = [ + { key: 'widthOptionsMm', label: 'Ширина', placeholder: 'Добавить ширину', kind: 'number', suffix: 'мм' }, + { key: 'lengthOptionsM', label: 'Длина', placeholder: 'Добавить длину', kind: 'number', suffix: 'м' }, + { key: 'thicknessOptionsMicron', label: 'Толщина', placeholder: 'Добавить толщину', kind: 'number', suffix: 'мкм' }, + { key: 'sleeveOptions', label: 'Втулка', placeholder: 'Добавить втулку', kind: 'text' }, + { key: 'colorOptions', label: 'Цвет', placeholder: 'Добавить цвет', kind: 'text' }, + { key: 'labelOptions', label: 'Надпись', placeholder: 'Добавить надпись', kind: 'text' }, +]; const settingsQuery = useQuery(CatalogProductTypeSettingsDocument); -const catalogProductsQuery = useQuery(ClientProductsDocument); const saveSettingMutation = useMutation(UpsertCatalogProductTypeSettingDocument, { throws: 'never' }); const forms = reactive>({}); @@ -42,64 +62,64 @@ const saveSuccess = ref(''); const saveError = ref(''); const settings = computed(() => settingsQuery.result.value?.catalogProductTypeSettings ?? []); -const isLoading = computed(() => settingsQuery.loading.value || catalogProductsQuery.loading.value); +const isLoading = computed(() => settingsQuery.loading.value); function normalizeText(value: string | null | undefined) { return String(value ?? '').replaceAll(/\s+/g, ' ').trim(); } -function formatNumberOptions(values: Array, suffix: string) { - return [...new Set(values.filter((value): value is number => typeof value === 'number'))] - .sort((a, b) => a - b) - .map((value) => `${value} ${suffix}`); -} - -function formatTextOptions(values: Array) { - return [...new Set(values.map((value) => normalizeText(value)).filter(Boolean))] - .sort((a, b) => a.localeCompare(b, 'ru')); -} - -const standardOptionsByType = computed>(() => { - const products = catalogProductsQuery.result.value?.clientProducts ?? []; - const grouped = new Map(); - - for (const product of products) { - const typeLabel = normalizeText(product.productType) || 'Без типа'; - const existing = grouped.get(typeLabel); - if (existing) { - existing.push(product); - } else { - grouped.set(typeLabel, [product]); - } - } - - return Object.fromEntries( - [...grouped.entries()].map(([typeLabel, items]) => { - const colorValues = formatTextOptions( - items.flatMap((product) => product.tags.filter((tag) => COLOR_TAGS.includes(normalizeText(tag)))), - ); - const labelValues = formatTextOptions( - items.flatMap((product) => product.tags.filter((tag) => LABEL_TAGS.includes(normalizeText(tag)))), - ); - - const optionGroups: StandardOptionGroup[] = [ - { label: 'Ширина', values: formatNumberOptions(items.map((product) => product.widthMm), 'мм') }, - { label: 'Длина', values: formatNumberOptions(items.map((product) => product.lengthM), 'м') }, - { label: 'Толщина', values: formatNumberOptions(items.map((product) => product.thicknessMicron), 'мкм') }, - { label: 'Втулка', values: formatTextOptions(items.map((product) => product.sleeveBrand)) }, - { label: 'Цвет', values: colorValues }, - { label: 'Надпись', values: labelValues }, - ].filter((group) => group.values.length > 0); - - return [typeLabel, optionGroups]; - }), - ); -}); - function toInputValue(value: number | null | undefined) { return value == null ? '' : String(value); } +function createDrafts(): Record { + return { + widthOptionsMm: '', + lengthOptionsM: '', + thicknessOptionsMicron: '', + sleeveOptions: '', + colorOptions: '', + labelOptions: '', + }; +} + +function parseOptionalInteger(value: string) { + const normalized = value.trim(); + if (!normalized) { + return null; + } + + const parsed = Number(normalized); + if (!Number.isInteger(parsed) || parsed <= 0) { + return null; + } + + return parsed; +} + +function normalizeOptionEntry(value: string, kind: OptionKind) { + if (kind === 'number') { + const parsed = parseOptionalInteger(value); + return parsed == null ? '' : String(parsed); + } + + return normalizeText(value); +} + +function normalizeOptionList(values: Array, kind: OptionKind) { + const normalizedValues = values + .map((value) => normalizeOptionEntry(String(value ?? ''), kind)) + .filter(Boolean); + + return [...new Set(normalizedValues)].sort((left, right) => { + if (kind === 'number') { + return Number(left) - Number(right); + } + + return left.localeCompare(right, 'ru'); + }); +} + function createForm(item: CatalogSettingItem): CatalogSettingForm { return { productType: item.productType, @@ -109,25 +129,45 @@ function createForm(item: CatalogSettingItem): CatalogSettingForm { customLengthStepM: toInputValue(item.customLengthStepM), allowCustomSleeveBrand: item.allowCustomSleeveBrand, allowCustomLabel: item.allowCustomLabel, + widthOptionsMm: normalizeOptionList(item.widthOptionsMm, 'number'), + lengthOptionsM: normalizeOptionList(item.lengthOptionsM, 'number'), + thicknessOptionsMicron: normalizeOptionList(item.thicknessOptionsMicron, 'number'), + sleeveOptions: normalizeOptionList(item.sleeveOptions, 'text'), + colorOptions: normalizeOptionList(item.colorOptions, 'text'), + labelOptions: normalizeOptionList(item.labelOptions, 'text'), + drafts: createDrafts(), }; } -function parseOptionalInteger(value: string) { - const normalized = value.trim(); - if (!normalized) { - return null; - } - - return Number(normalized); -} - function formFor(item: CatalogSettingItem) { forms[item.productType] ??= createForm(item); return forms[item.productType]; } -function standardOptionsFor(item: CatalogSettingItem) { - return standardOptionsByType.value[item.productType] ?? []; +function addOption(form: CatalogSettingForm, group: OptionGroupDefinition) { + const value = normalizeOptionEntry(form.drafts[group.key], group.kind); + if (!value) { + return; + } + + form[group.key] = normalizeOptionList([...form[group.key], value], group.kind); + form.drafts[group.key] = ''; +} + +function removeOption(form: CatalogSettingForm, groupKey: OptionKey, value: string) { + form[groupKey] = form[groupKey].filter((item) => item !== value); +} + +function optionChipLabel(value: string, group: OptionGroupDefinition) { + return group.suffix ? `${value} ${group.suffix}` : value; +} + +function parseIntegerOptionList(values: string[]) { + return normalizeOptionList(values, 'number').map((value) => Number(value)); +} + +function parseTextOptionList(values: string[]) { + return normalizeOptionList(values, 'text'); } watch( @@ -165,6 +205,12 @@ async function saveAllSettings() { customLengthStepM: form.allowCustomLength ? parseOptionalInteger(form.customLengthStepM) : null, allowCustomSleeveBrand: form.allowCustomSleeveBrand, allowCustomLabel: form.allowCustomLabel, + widthOptionsMm: parseIntegerOptionList(form.widthOptionsMm), + lengthOptionsM: parseIntegerOptionList(form.lengthOptionsM), + thicknessOptionsMicron: parseIntegerOptionList(form.thicknessOptionsMicron), + sleeveOptions: parseTextOptionList(form.sleeveOptions), + colorOptions: parseTextOptionList(form.colorOptions), + labelOptions: parseTextOptionList(form.labelOptions), }, }); @@ -194,13 +240,13 @@ async function saveAllSettings() { Типы товаров пока не появились в каталоге. -
+
-
+

{{ item.productType }}

@@ -258,23 +304,56 @@ async function saveAllSettings() {
-
+
Стандартные параметры
-
+ +
-
{{ group.label }}
-
- - {{ value }} - +
+
{{ group.label }}
+ +
+ + + + Пока пусто + +
+ +
+ + +
diff --git a/graphql/operations/settings/catalog-product-type-settings.graphql b/graphql/operations/settings/catalog-product-type-settings.graphql index 01e8946..304d435 100644 --- a/graphql/operations/settings/catalog-product-type-settings.graphql +++ b/graphql/operations/settings/catalog-product-type-settings.graphql @@ -8,5 +8,11 @@ query CatalogProductTypeSettings { customLengthStepM allowCustomSleeveBrand allowCustomLabel + widthOptionsMm + lengthOptionsM + thicknessOptionsMicron + sleeveOptions + colorOptions + labelOptions } } diff --git a/graphql/operations/settings/upsert-catalog-product-type-setting.graphql b/graphql/operations/settings/upsert-catalog-product-type-setting.graphql index d976373..e0d91cb 100644 --- a/graphql/operations/settings/upsert-catalog-product-type-setting.graphql +++ b/graphql/operations/settings/upsert-catalog-product-type-setting.graphql @@ -8,5 +8,11 @@ mutation UpsertCatalogProductTypeSetting($input: UpsertCatalogProductTypeSetting customLengthStepM allowCustomSleeveBrand allowCustomLabel + widthOptionsMm + lengthOptionsM + thicknessOptionsMicron + sleeveOptions + colorOptions + labelOptions } } diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 38bb0bd..245548a 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -257,6 +257,12 @@ type CatalogProductTypeSetting { customLengthStepM: Int allowCustomSleeveBrand: Boolean! allowCustomLabel: Boolean! + widthOptionsMm: [Int!]! + lengthOptionsM: [Int!]! + thicknessOptionsMicron: [Int!]! + sleeveOptions: [String!]! + colorOptions: [String!]! + labelOptions: [String!]! } type CartItem { @@ -512,6 +518,12 @@ input UpsertCatalogProductTypeSettingInput { customLengthStepM: Int allowCustomSleeveBrand: Boolean! allowCustomLabel: Boolean! + widthOptionsMm: [Int!]! + lengthOptionsM: [Int!]! + thicknessOptionsMicron: [Int!]! + sleeveOptions: [String!]! + colorOptions: [String!]! + labelOptions: [String!]! } input ReadyOrderItemInput {