From 0103c3fb8a27c541d1254820e4ed867f475e120a Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:10:52 +0700 Subject: [PATCH] Add catalog option sets --- .../migration.sql | 14 +++ prisma/schema.prisma | 6 + src/resolvers.js | 113 +++++++++++++++--- src/schema.graphql | 12 ++ 4 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/0012_add_catalog_product_type_option_sets/migration.sql diff --git a/prisma/migrations/0012_add_catalog_product_type_option_sets/migration.sql b/prisma/migrations/0012_add_catalog_product_type_option_sets/migration.sql new file mode 100644 index 0000000..d5f0d18 --- /dev/null +++ b/prisma/migrations/0012_add_catalog_product_type_option_sets/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "CatalogProductTypeSetting" ADD COLUMN "colorOptions" TEXT[] DEFAULT ARRAY[]::TEXT[], +ADD COLUMN "labelOptions" TEXT[] DEFAULT ARRAY[]::TEXT[], +ADD COLUMN "lengthOptionsM" INTEGER[] DEFAULT ARRAY[]::INTEGER[], +ADD COLUMN "sleeveOptions" TEXT[] DEFAULT ARRAY[]::TEXT[], +ADD COLUMN "thicknessOptionsMicron" INTEGER[] DEFAULT ARRAY[]::INTEGER[], +ADD COLUMN "widthOptionsMm" INTEGER[] DEFAULT ARRAY[]::INTEGER[]; +-- AlterTable +ALTER TABLE "CatalogProductTypeSetting" ADD COLUMN "colorOptions" TEXT[] DEFAULT ARRAY[]::TEXT[], +ADD COLUMN "labelOptions" TEXT[] DEFAULT ARRAY[]::TEXT[], +ADD COLUMN "lengthOptionsM" INTEGER[] DEFAULT ARRAY[]::INTEGER[], +ADD COLUMN "sleeveOptions" TEXT[] DEFAULT ARRAY[]::TEXT[], +ADD COLUMN "thicknessOptionsMicron" INTEGER[] DEFAULT ARRAY[]::INTEGER[], +ADD COLUMN "widthOptionsMm" INTEGER[] DEFAULT ARRAY[]::INTEGER[]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1c0f18f..91dbccf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -200,6 +200,12 @@ model CatalogProductTypeSetting { customLengthStepM Int? allowCustomSleeveBrand Boolean @default(false) allowCustomLabel Boolean @default(false) + widthOptionsMm Int[] @default([]) + lengthOptionsM Int[] @default([]) + thicknessOptionsMicron Int[] @default([]) + sleeveOptions String[] @default([]) + colorOptions String[] @default([]) + labelOptions String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/resolvers.js b/src/resolvers.js index aa19999..2d9c5be 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -441,6 +441,57 @@ function presentCatalogProductTypeSetting(setting) { customLengthStepM: setting.customLengthStepM ?? null, allowCustomSleeveBrand: Boolean(setting.allowCustomSleeveBrand), allowCustomLabel: Boolean(setting.allowCustomLabel), + widthOptionsMm: [...(setting.widthOptionsMm ?? [])], + lengthOptionsM: [...(setting.lengthOptionsM ?? [])], + thicknessOptionsMicron: [...(setting.thicknessOptionsMicron ?? [])], + sleeveOptions: [...(setting.sleeveOptions ?? [])], + colorOptions: [...(setting.colorOptions ?? [])], + labelOptions: [...(setting.labelOptions ?? [])], + }; +} + +function normalizePositiveIntList(values) { + return [...new Set((values ?? []) + .map((value) => normalizeOptionalPositiveInt(value)) + .filter((value) => value !== null))] + .sort((a, b) => a - b); +} + +function normalizeTextList(values) { + return [...new Set((values ?? []) + .map((value) => normalizeText(value)) + .filter(Boolean))] + .sort((a, b) => a.localeCompare(b, 'ru')); +} + +function collectProductTypeOptionDefaults(products) { + const colorOptions = []; + const labelOptions = []; + + for (const product of products) { + for (const tag of product.tags ?? []) { + const normalizedTag = normalizeText(tag); + if (!normalizedTag) { + continue; + } + + if (['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый'].includes(normalizedTag)) { + colorOptions.push(normalizedTag); + } + + if (['хрупкое', 'подарок', 'акция'].includes(normalizedTag)) { + labelOptions.push(normalizedTag); + } + } + } + + return { + widthOptionsMm: normalizePositiveIntList(products.map((product) => product.widthMm)), + lengthOptionsM: normalizePositiveIntList(products.map((product) => product.lengthM)), + thicknessOptionsMicron: normalizePositiveIntList(products.map((product) => product.thicknessMicron)), + sleeveOptions: normalizeTextList(products.map((product) => product.sleeveBrand)), + colorOptions: normalizeTextList(colorOptions), + labelOptions: normalizeTextList(labelOptions), }; } @@ -474,6 +525,12 @@ function normalizeCatalogProductTypeSettingInput(input) { customLengthStepM, allowCustomSleeveBrand: Boolean(input.allowCustomSleeveBrand), allowCustomLabel: Boolean(input.allowCustomLabel), + widthOptionsMm: normalizePositiveIntList(input.widthOptionsMm), + lengthOptionsM: normalizePositiveIntList(input.lengthOptionsM), + thicknessOptionsMicron: normalizePositiveIntList(input.thicknessOptionsMicron), + sleeveOptions: normalizeTextList(input.sleeveOptions), + colorOptions: normalizeTextList(input.colorOptions), + labelOptions: normalizeTextList(input.labelOptions), }; } @@ -486,34 +543,60 @@ async function listCatalogProductTypeSettings(prisma) { productType: null, }, }, - select: { productType: true }, - distinct: ['productType'], - orderBy: { productType: 'asc' }, + select: { + productType: true, + widthMm: true, + lengthM: true, + thicknessMicron: true, + sleeveBrand: true, + tags: true, + }, + orderBy: [{ productType: 'asc' }, { name: 'asc' }], }), prisma.catalogProductTypeSetting.findMany({ orderBy: { productType: 'asc' }, }), ]); + const productsByType = new Map(); + for (const product of products) { + const productType = normalizeText(product.productType); + if (!productType) { + continue; + } + + const existing = productsByType.get(productType); + if (existing) { + existing.push(product); + } else { + productsByType.set(productType, [product]); + } + } + const productTypes = new Set([ - ...products.map((item) => normalizeText(item.productType)).filter(Boolean), + ...productsByType.keys(), ...persistedSettings.map((item) => item.productType), ]); const settingsByType = new Map(persistedSettings.map((item) => [item.productType, item])); return [...productTypes] .sort((a, b) => a.localeCompare(b, 'ru')) - .map((productType) => presentCatalogProductTypeSetting({ - productType, - showQuantityPerBox: false, - allowCustomLength: false, - customLengthMinM: null, - customLengthMaxM: null, - customLengthStepM: null, - allowCustomSleeveBrand: false, - allowCustomLabel: false, - ...settingsByType.get(productType), - })); + .map((productType) => { + const persisted = settingsByType.get(productType); + const fallbackOptions = collectProductTypeOptionDefaults(productsByType.get(productType) ?? []); + + return presentCatalogProductTypeSetting(persisted ?? { + productType, + showQuantityPerBox: false, + allowCustomLength: false, + customLengthMinM: null, + customLengthMaxM: null, + customLengthStepM: null, + allowCustomSleeveBrand: false, + allowCustomLabel: false, + ...fallbackOptions, + }); + }); } const cartInclude = { diff --git a/src/schema.graphql b/src/schema.graphql index 38bb0bd..245548a 100644 --- a/src/schema.graphql +++ b/src/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 {