Show standard catalog options in settings
This commit is contained in:
@@ -2,8 +2,10 @@
|
|||||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||||
import {
|
import {
|
||||||
CatalogProductTypeSettingsDocument,
|
CatalogProductTypeSettingsDocument,
|
||||||
|
ClientProductsDocument,
|
||||||
UpsertCatalogProductTypeSettingDocument,
|
UpsertCatalogProductTypeSettingDocument,
|
||||||
type CatalogProductTypeSettingsQuery,
|
type CatalogProductTypeSettingsQuery,
|
||||||
|
type ClientProductsQuery,
|
||||||
} from '~/composables/graphql/generated';
|
} from '~/composables/graphql/generated';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -12,6 +14,7 @@ definePageMeta({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type CatalogSettingItem = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
|
type CatalogSettingItem = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
|
||||||
|
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
||||||
type CatalogSettingForm = {
|
type CatalogSettingForm = {
|
||||||
productType: string;
|
productType: string;
|
||||||
allowCustomLength: boolean;
|
allowCustomLength: boolean;
|
||||||
@@ -21,8 +24,16 @@ type CatalogSettingForm = {
|
|||||||
allowCustomSleeveBrand: boolean;
|
allowCustomSleeveBrand: boolean;
|
||||||
allowCustomLabel: boolean;
|
allowCustomLabel: boolean;
|
||||||
};
|
};
|
||||||
|
type StandardOptionGroup = {
|
||||||
|
label: string;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLOR_TAGS = ['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый'];
|
||||||
|
const LABEL_TAGS = ['хрупкое', 'подарок', 'акция'];
|
||||||
|
|
||||||
const settingsQuery = useQuery(CatalogProductTypeSettingsDocument);
|
const settingsQuery = useQuery(CatalogProductTypeSettingsDocument);
|
||||||
|
const catalogProductsQuery = useQuery(ClientProductsDocument);
|
||||||
const saveSettingMutation = useMutation(UpsertCatalogProductTypeSettingDocument, { throws: 'never' });
|
const saveSettingMutation = useMutation(UpsertCatalogProductTypeSettingDocument, { throws: 'never' });
|
||||||
|
|
||||||
const forms = reactive<Record<string, CatalogSettingForm>>({});
|
const forms = reactive<Record<string, CatalogSettingForm>>({});
|
||||||
@@ -31,6 +42,59 @@ const saveSuccess = ref('');
|
|||||||
const saveError = ref('');
|
const saveError = ref('');
|
||||||
|
|
||||||
const settings = computed<CatalogSettingItem[]>(() => settingsQuery.result.value?.catalogProductTypeSettings ?? []);
|
const settings = computed<CatalogSettingItem[]>(() => settingsQuery.result.value?.catalogProductTypeSettings ?? []);
|
||||||
|
const isLoading = computed(() => settingsQuery.loading.value || catalogProductsQuery.loading.value);
|
||||||
|
|
||||||
|
function normalizeText(value: string | null | undefined) {
|
||||||
|
return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumberOptions(values: Array<number | null | undefined>, 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<string | null | undefined>) {
|
||||||
|
return [...new Set(values.map((value) => normalizeText(value)).filter(Boolean))]
|
||||||
|
.sort((a, b) => a.localeCompare(b, 'ru'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const standardOptionsByType = computed<Record<string, StandardOptionGroup[]>>(() => {
|
||||||
|
const products = catalogProductsQuery.result.value?.clientProducts ?? [];
|
||||||
|
const grouped = new Map<string, ProductNode[]>();
|
||||||
|
|
||||||
|
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) {
|
function toInputValue(value: number | null | undefined) {
|
||||||
return value == null ? '' : String(value);
|
return value == null ? '' : String(value);
|
||||||
@@ -62,6 +126,10 @@ function formFor(item: CatalogSettingItem) {
|
|||||||
return forms[item.productType];
|
return forms[item.productType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function standardOptionsFor(item: CatalogSettingItem) {
|
||||||
|
return standardOptionsByType.value[item.productType] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
(items) => {
|
(items) => {
|
||||||
@@ -118,7 +186,7 @@ async function saveAllSettings() {
|
|||||||
<section class="space-y-6">
|
<section class="space-y-6">
|
||||||
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
|
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
|
||||||
|
|
||||||
<div v-if="settingsQuery.loading.value" class="manager-empty-state">
|
<div v-if="isLoading" class="manager-empty-state">
|
||||||
Загружаем настройки каталога...
|
Загружаем настройки каталога...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,20 +203,20 @@ async function saveAllSettings() {
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<h2 class="text-xl font-bold text-[#123824]">{{ item.productType }}</h2>
|
<h2 class="text-xl font-bold text-[#123824]">{{ item.productType }}</h2>
|
||||||
|
|
||||||
<div class="grid gap-3 md:grid-cols-2">
|
<div class="space-y-3">
|
||||||
<label class="surface-card flex items-center justify-between gap-4 rounded-[22px] p-4">
|
<label class="surface-card flex items-center gap-3 rounded-[22px] p-4">
|
||||||
|
<input v-model="formFor(item).allowCustomLength" type="checkbox" class="checkbox checkbox-success">
|
||||||
<span class="text-sm font-semibold text-[#123824]">Любая длина</span>
|
<span class="text-sm font-semibold text-[#123824]">Любая длина</span>
|
||||||
<input v-model="formFor(item).allowCustomLength" type="checkbox" class="toggle toggle-success">
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="surface-card flex items-center justify-between gap-4 rounded-[22px] p-4">
|
<label class="surface-card flex items-center gap-3 rounded-[22px] p-4">
|
||||||
<span class="text-sm font-semibold text-[#123824]">Втулка с логотипом</span>
|
<input v-model="formFor(item).allowCustomSleeveBrand" type="checkbox" class="checkbox checkbox-success">
|
||||||
<input v-model="formFor(item).allowCustomSleeveBrand" type="checkbox" class="toggle toggle-success">
|
<span class="text-sm font-semibold text-[#123824]">Логотип на втулке</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="surface-card flex items-center justify-between gap-4 rounded-[22px] p-4">
|
<label class="surface-card flex items-center gap-3 rounded-[22px] p-4">
|
||||||
|
<input v-model="formFor(item).allowCustomLabel" type="checkbox" class="checkbox checkbox-success">
|
||||||
<span class="text-sm font-semibold text-[#123824]">Нанесение надписи</span>
|
<span class="text-sm font-semibold text-[#123824]">Нанесение надписи</span>
|
||||||
<input v-model="formFor(item).allowCustomLabel" type="checkbox" class="toggle toggle-success">
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -189,6 +257,28 @@ async function saveAllSettings() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="standardOptionsFor(item).length" class="rounded-[24px] bg-[#f7fbf8] p-4">
|
||||||
|
<div class="mb-4 text-sm font-bold uppercase tracking-[0.12em] text-[#355947]">Стандартные параметры</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="group in standardOptionsFor(item)"
|
||||||
|
:key="`${item.productType}-${group.label}`"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<div class="text-sm font-semibold text-[#123824]">{{ group.label }}</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="value in group.values"
|
||||||
|
:key="`${item.productType}-${group.label}-${value}`"
|
||||||
|
class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#355947]"
|
||||||
|
>
|
||||||
|
{{ value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user