Add catalog settings management
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user