Add catalog option sets

This commit is contained in:
Ruslan Bakiev
2026-04-09 17:10:52 +07:00
parent 2cd8d0b612
commit 0103c3fb8a
4 changed files with 130 additions and 15 deletions

View File

@@ -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[];

View File

@@ -200,6 +200,12 @@ model CatalogProductTypeSetting {
customLengthStepM Int? customLengthStepM Int?
allowCustomSleeveBrand Boolean @default(false) allowCustomSleeveBrand Boolean @default(false)
allowCustomLabel 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()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }

View File

@@ -441,6 +441,57 @@ function presentCatalogProductTypeSetting(setting) {
customLengthStepM: setting.customLengthStepM ?? null, customLengthStepM: setting.customLengthStepM ?? null,
allowCustomSleeveBrand: Boolean(setting.allowCustomSleeveBrand), allowCustomSleeveBrand: Boolean(setting.allowCustomSleeveBrand),
allowCustomLabel: Boolean(setting.allowCustomLabel), 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, customLengthStepM,
allowCustomSleeveBrand: Boolean(input.allowCustomSleeveBrand), allowCustomSleeveBrand: Boolean(input.allowCustomSleeveBrand),
allowCustomLabel: Boolean(input.allowCustomLabel), 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,24 +543,49 @@ async function listCatalogProductTypeSettings(prisma) {
productType: null, productType: null,
}, },
}, },
select: { productType: true }, select: {
distinct: ['productType'], productType: true,
orderBy: { productType: 'asc' }, widthMm: true,
lengthM: true,
thicknessMicron: true,
sleeveBrand: true,
tags: true,
},
orderBy: [{ productType: 'asc' }, { name: 'asc' }],
}), }),
prisma.catalogProductTypeSetting.findMany({ prisma.catalogProductTypeSetting.findMany({
orderBy: { productType: 'asc' }, 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([ const productTypes = new Set([
...products.map((item) => normalizeText(item.productType)).filter(Boolean), ...productsByType.keys(),
...persistedSettings.map((item) => item.productType), ...persistedSettings.map((item) => item.productType),
]); ]);
const settingsByType = new Map(persistedSettings.map((item) => [item.productType, item])); const settingsByType = new Map(persistedSettings.map((item) => [item.productType, item]));
return [...productTypes] return [...productTypes]
.sort((a, b) => a.localeCompare(b, 'ru')) .sort((a, b) => a.localeCompare(b, 'ru'))
.map((productType) => presentCatalogProductTypeSetting({ .map((productType) => {
const persisted = settingsByType.get(productType);
const fallbackOptions = collectProductTypeOptionDefaults(productsByType.get(productType) ?? []);
return presentCatalogProductTypeSetting(persisted ?? {
productType, productType,
showQuantityPerBox: false, showQuantityPerBox: false,
allowCustomLength: false, allowCustomLength: false,
@@ -512,8 +594,9 @@ async function listCatalogProductTypeSettings(prisma) {
customLengthStepM: null, customLengthStepM: null,
allowCustomSleeveBrand: false, allowCustomSleeveBrand: false,
allowCustomLabel: false, allowCustomLabel: false,
...settingsByType.get(productType), ...fallbackOptions,
})); });
});
} }
const cartInclude = { const cartInclude = {

View File

@@ -257,6 +257,12 @@ type CatalogProductTypeSetting {
customLengthStepM: Int customLengthStepM: Int
allowCustomSleeveBrand: Boolean! allowCustomSleeveBrand: Boolean!
allowCustomLabel: Boolean! allowCustomLabel: Boolean!
widthOptionsMm: [Int!]!
lengthOptionsM: [Int!]!
thicknessOptionsMicron: [Int!]!
sleeveOptions: [String!]!
colorOptions: [String!]!
labelOptions: [String!]!
} }
type CartItem { type CartItem {
@@ -512,6 +518,12 @@ input UpsertCatalogProductTypeSettingInput {
customLengthStepM: Int customLengthStepM: Int
allowCustomSleeveBrand: Boolean! allowCustomSleeveBrand: Boolean!
allowCustomLabel: Boolean! allowCustomLabel: Boolean!
widthOptionsMm: [Int!]!
lengthOptionsM: [Int!]!
thicknessOptionsMicron: [Int!]!
sleeveOptions: [String!]!
colorOptions: [String!]!
labelOptions: [String!]!
} }
input ReadyOrderItemInput { input ReadyOrderItemInput {