Add catalog settings management

This commit is contained in:
Ruslan Bakiev
2026-04-09 16:03:32 +07:00
parent e8fbe84e4f
commit 7ed5fbd66d
7 changed files with 504 additions and 9 deletions

View File

@@ -1,9 +1,15 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import { ClientProductsDocument, type ClientProductsQuery } from '~/composables/graphql/generated';
import {
CatalogProductTypeSettingsDocument,
ClientProductsDocument,
type CatalogProductTypeSettingsQuery,
type ClientProductsQuery,
} from '~/composables/graphql/generated';
import { useClientCart } from '~/composables/useClientCart';
type ProductNode = ClientProductsQuery['clientProducts'][number];
type CatalogProductTypeSettingNode = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
type ParamFieldKey = 'widthMm' | 'lengthM' | 'thicknessMicron' | 'sleeveBrand' | 'quantityPerBox' | 'colorTag' | 'labelTag';
type ParamValue = number | string;
@@ -35,6 +41,16 @@ type GroupState = {
const COLOR_TAGS = ['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый'];
const LABEL_TAGS = ['хрупкое', 'подарок', 'акция'];
const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand', 'quantityPerBox', 'colorTag', 'labelTag'];
const DEFAULT_CATALOG_PRODUCT_TYPE_SETTING: CatalogProductTypeSettingNode = {
productType: '',
showQuantityPerBox: false,
allowCustomLength: false,
customLengthMinM: null,
customLengthMaxM: null,
customLengthStepM: null,
allowCustomSleeveBrand: false,
allowCustomLabel: false,
};
const parameterFields: Array<{ key: ParamFieldKey; label: string }> = [
{ key: 'widthMm', label: 'Ширина' },
{ key: 'lengthM', label: 'Длина' },
@@ -51,11 +67,22 @@ const coverPresets = [
['#e8f5ec', '#b2e0c6', '#7dd0a9'],
];
const { result, loading, error } = useQuery(ClientProductsDocument);
const productsQuery = useQuery(ClientProductsDocument);
const catalogSettingsQuery = useQuery(CatalogProductTypeSettingsDocument);
const search = ref('');
const groupStates = reactive<Record<string, GroupState>>({});
const { addProduct, getQuantity, incrementQuantity, decrementQuantity } = useClientCart();
const loading = computed(() => productsQuery.loading.value || catalogSettingsQuery.loading.value);
const error = computed(() => productsQuery.error.value || catalogSettingsQuery.error.value);
const catalogSettingsByType = computed<Record<string, CatalogProductTypeSettingNode>>(() => (
Object.fromEntries(
(catalogSettingsQuery.result.value?.catalogProductTypeSettings ?? [])
.map((setting) => [setting.productType, setting]),
)
));
function normalizeText(value: string | null | undefined) {
return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
}
@@ -135,7 +162,7 @@ function compareProducts(a: ParsedProduct, b: ParsedProduct) {
}
const parsedProducts = computed<ParsedProduct[]>(() => {
const list = result.value?.clientProducts ?? [];
const list = productsQuery.result.value?.clientProducts ?? [];
const query = search.value.trim().toLowerCase();
return list
@@ -228,8 +255,23 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) {
return sortParamValues([...values]);
}
function groupCatalogSetting(group: ProductGroup) {
return catalogSettingsByType.value[group.typeLabel] ?? {
...DEFAULT_CATALOG_PRODUCT_TYPE_SETTING,
productType: group.typeLabel,
};
}
function visibleFields(group: ProductGroup) {
return parameterFields.filter((field) => getAllFieldOptions(group, field.key).length > 1);
const catalogSetting = groupCatalogSetting(group);
return parameterFields.filter((field) => {
if (field.key === 'quantityPerBox' && !catalogSetting.showQuantityPerBox) {
return false;
}
return getAllFieldOptions(group, field.key).length > 1;
});
}
function requiredKeys(group: ProductGroup) {
@@ -301,7 +343,7 @@ function selectedProduct(group: ProductGroup) {
const state = getGroupState(group);
if (keys.length === 0) {
return group.products.length === 1 ? group.products[0] : null;
return group.products[0] ?? null;
}
if (keys.some((key) => state[key] === null)) {
@@ -309,7 +351,7 @@ function selectedProduct(group: ProductGroup) {
}
const matches = group.products.filter((product) => matchesProductState(product, state, keys));
return matches.length === 1 ? matches[0] : null;
return matches[0] ?? null;
}
function formatOptionLabel(field: ParamFieldKey, value: ParamValue) {
@@ -441,6 +483,25 @@ function variantCountLabel(count: number) {
return `${count} вариантов`;
}
function customizationNotes(group: ProductGroup) {
const setting = groupCatalogSetting(group);
const notes: string[] = [];
if (setting.allowCustomLength && setting.customLengthMinM && setting.customLengthMaxM && setting.customLengthStepM) {
notes.push(`Своя длина ${setting.customLengthMinM}-${setting.customLengthMaxM} м, шаг ${setting.customLengthStepM} м`);
}
if (setting.allowCustomSleeveBrand) {
notes.push('Своя втулка');
}
if (setting.allowCustomLabel) {
notes.push('Своя надпись');
}
return notes;
}
function toggleExpanded(group: ProductGroup) {
getGroupState(group).isExpanded = !getGroupState(group).isExpanded;
}
@@ -513,6 +574,15 @@ function decrementSelected(group: ProductGroup) {
<div class="p-4 md:p-5 xl:col-span-4">
<div class="mb-4">
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
<div v-if="customizationNotes(group).length" class="mt-3 flex flex-wrap gap-2">
<span
v-for="note in customizationNotes(group)"
:key="`${group.key}-${note}`"
class="rounded-full bg-[#eef8f1] px-3 py-1 text-xs font-semibold text-[#15613d]"
>
{{ note }}
</span>
</div>
</div>
<div class="grid content-start gap-4 md:grid-cols-2 2xl:grid-cols-3">
@@ -599,7 +669,7 @@ function decrementSelected(group: ProductGroup) {
<th class="border-b border-base-300">Длина</th>
<th class="border-b border-base-300">Толщина</th>
<th class="border-b border-base-300">Втулка</th>
<th class="border-b border-base-300">Короб</th>
<th v-if="groupCatalogSetting(group).showQuantityPerBox" class="border-b border-base-300">Короб</th>
<th class="border-b border-base-300 text-right">Действие</th>
</tr>
</thead>
@@ -610,7 +680,7 @@ function decrementSelected(group: ProductGroup) {
<td class="border-b border-base-200">{{ product.lengthM ?? '—' }}</td>
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
<td class="border-b border-base-200">{{ product.quantityPerBox ?? '—' }}</td>
<td v-if="groupCatalogSetting(group).showQuantityPerBox" class="border-b border-base-200">{{ product.quantityPerBox ?? '' }}</td>
<td class="border-b border-base-200 text-right">
<button
v-if="getQuantity(product.id) === 0"