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

@@ -0,0 +1,257 @@
<script setup lang="ts">
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
CatalogProductTypeSettingsDocument,
UpsertCatalogProductTypeSettingDocument,
type CatalogProductTypeSettingsQuery,
} from '~/composables/graphql/generated';
definePageMeta({
middleware: ['manager-only'],
path: '/admin/settings/catalog',
});
type CatalogSettingItem = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
type CatalogSettingForm = {
productType: string;
showQuantityPerBox: boolean;
allowCustomLength: boolean;
customLengthMinM: string;
customLengthMaxM: string;
customLengthStepM: string;
allowCustomSleeveBrand: boolean;
allowCustomLabel: boolean;
};
const settingsQuery = useQuery(CatalogProductTypeSettingsDocument);
const saveSettingMutation = useMutation(UpsertCatalogProductTypeSettingDocument, { throws: 'never' });
const forms = reactive<Record<string, CatalogSettingForm>>({});
const savingState = reactive<Record<string, boolean>>({});
const successMessage = reactive<Record<string, string>>({});
const errorMessage = reactive<Record<string, string>>({});
const settings = computed<CatalogSettingItem[]>(() => settingsQuery.result.value?.catalogProductTypeSettings ?? []);
function toInputValue(value: number | null | undefined) {
return value == null ? '' : String(value);
}
function createForm(item: CatalogSettingItem): CatalogSettingForm {
return {
productType: item.productType,
showQuantityPerBox: item.showQuantityPerBox,
allowCustomLength: item.allowCustomLength,
customLengthMinM: toInputValue(item.customLengthMinM),
customLengthMaxM: toInputValue(item.customLengthMaxM),
customLengthStepM: toInputValue(item.customLengthStepM),
allowCustomSleeveBrand: item.allowCustomSleeveBrand,
allowCustomLabel: item.allowCustomLabel,
};
}
function parseOptionalInteger(value: string) {
const normalized = value.trim();
if (!normalized) {
return null;
}
return Number(normalized);
}
watch(
settings,
(items) => {
const activeTypes = new Set(items.map((item) => item.productType));
for (const productType of Object.keys(forms)) {
if (!activeTypes.has(productType)) {
Reflect.deleteProperty(forms, productType);
Reflect.deleteProperty(savingState, productType);
Reflect.deleteProperty(successMessage, productType);
Reflect.deleteProperty(errorMessage, productType);
}
}
for (const item of items) {
forms[item.productType] = createForm(item);
savingState[item.productType] ??= false;
successMessage[item.productType] ??= '';
errorMessage[item.productType] ??= '';
}
},
{ immediate: true },
);
async function saveProductTypeSetting(productType: string) {
const form = forms[productType];
if (!form) {
return;
}
successMessage[productType] = '';
errorMessage[productType] = '';
savingState[productType] = true;
const result = await saveSettingMutation.mutate({
input: {
productType: form.productType,
showQuantityPerBox: form.showQuantityPerBox,
allowCustomLength: form.allowCustomLength,
customLengthMinM: form.allowCustomLength ? parseOptionalInteger(form.customLengthMinM) : null,
customLengthMaxM: form.allowCustomLength ? parseOptionalInteger(form.customLengthMaxM) : null,
customLengthStepM: form.allowCustomLength ? parseOptionalInteger(form.customLengthStepM) : null,
allowCustomSleeveBrand: form.allowCustomSleeveBrand,
allowCustomLabel: form.allowCustomLabel,
},
});
savingState[productType] = false;
if (!result?.data?.upsertCatalogProductTypeSetting) {
errorMessage[productType] = saveSettingMutation.error.value?.message || 'Не удалось сохранить настройки.';
return;
}
forms[productType] = createForm(result.data.upsertCatalogProductTypeSetting);
successMessage[productType] = 'Настройки сохранены.';
}
</script>
<template>
<section class="space-y-6">
<div class="space-y-2">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
<p class="text-sm leading-6 text-[#557562]">
Правила конструктора по каждому типу товара: длина, своя втулка, своя надпись и видимость короба.
</p>
</div>
<div v-if="settingsQuery.loading.value" class="manager-empty-state">
Загружаем настройки каталога...
</div>
<div v-else-if="settings.length === 0" class="manager-empty-state">
Типы товаров пока не появились в каталоге.
</div>
<div v-else class="grid gap-4 xl:grid-cols-2">
<article
v-for="item in settings"
:key="item.productType"
class="rounded-[28px] bg-white p-5 shadow-[0_18px_38px_rgba(18,56,36,0.08)]"
>
<div class="flex flex-col gap-4">
<div class="space-y-1">
<h2 class="text-xl font-bold text-[#123824]">{{ item.productType }}</h2>
<p class="text-sm text-[#557562]">
Определяет, какие параметры клиент сможет менять на витрине для этого типа товара.
</p>
</div>
<div class="grid gap-3 md:grid-cols-2">
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
<input v-model="forms[item.productType].allowCustomLength" type="checkbox" class="toggle toggle-success mt-1">
<span>
<span class="block font-semibold text-[#123824]">Своя длина</span>
<span class="mt-1 block text-sm text-[#557562]">Разрешить длину вне стандартного списка.</span>
</span>
</label>
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
<input v-model="forms[item.productType].allowCustomSleeveBrand" type="checkbox" class="toggle toggle-success mt-1">
<span>
<span class="block font-semibold text-[#123824]">Своя втулка</span>
<span class="mt-1 block text-sm text-[#557562]">Клиент сможет запросить втулку со своим вариантом.</span>
</span>
</label>
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
<input v-model="forms[item.productType].allowCustomLabel" type="checkbox" class="toggle toggle-success mt-1">
<span>
<span class="block font-semibold text-[#123824]">Своя надпись</span>
<span class="mt-1 block text-sm text-[#557562]">Разрешить индивидуальную надпись или маркировку.</span>
</span>
</label>
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
<input v-model="forms[item.productType].showQuantityPerBox" type="checkbox" class="toggle toggle-success mt-1">
<span>
<span class="block font-semibold text-[#123824]">Показывать короб</span>
<span class="mt-1 block text-sm text-[#557562]">Если выключено, параметр короба скрывается из клиентского каталога.</span>
</span>
</label>
</div>
<div class="rounded-[24px] bg-[#f7fbf8] p-4">
<div class="flex items-center justify-between gap-3">
<div>
<h3 class="text-sm font-bold uppercase tracking-[0.12em] text-[#355947]">Диапазон длины</h3>
<p class="mt-1 text-sm text-[#557562]">Минимум, максимум и шаг для пользовательской длины.</p>
</div>
<span
class="rounded-full px-3 py-1 text-xs font-bold uppercase tracking-[0.12em]"
:class="forms[item.productType].allowCustomLength ? 'bg-[#e8f5ec] text-[#1c6b45]' : 'bg-[#eef2ef] text-[#6b7f71]'"
>
{{ forms[item.productType].allowCustomLength ? 'Включено' : 'Выключено' }}
</span>
</div>
<div class="mt-4 grid gap-3 md:grid-cols-3">
<label class="space-y-2">
<span class="text-sm font-semibold text-[#123824]">Мин. длина, м</span>
<input
v-model="forms[item.productType].customLengthMinM"
type="number"
min="1"
step="1"
class="input manager-field w-full"
:disabled="!forms[item.productType].allowCustomLength"
>
</label>
<label class="space-y-2">
<span class="text-sm font-semibold text-[#123824]">Макс. длина, м</span>
<input
v-model="forms[item.productType].customLengthMaxM"
type="number"
min="1"
step="1"
class="input manager-field w-full"
:disabled="!forms[item.productType].allowCustomLength"
>
</label>
<label class="space-y-2">
<span class="text-sm font-semibold text-[#123824]">Шаг, м</span>
<input
v-model="forms[item.productType].customLengthStepM"
type="number"
min="1"
step="1"
class="input manager-field w-full"
:disabled="!forms[item.productType].allowCustomLength"
>
</label>
</div>
</div>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="space-y-1 text-sm">
<p v-if="successMessage[item.productType]" class="font-semibold text-[#1c6b45]">{{ successMessage[item.productType] }}</p>
<p v-if="errorMessage[item.productType]" class="font-semibold text-[#c4472d]">{{ errorMessage[item.productType] }}</p>
</div>
<button
class="btn h-11 rounded-full border-0 bg-[#139957] px-5 text-sm font-semibold text-white hover:bg-[#0d854a]"
:disabled="savingState[item.productType]"
@click="saveProductTypeSetting(item.productType)"
>
{{ savingState[item.productType] ? 'Сохраняем…' : 'Сохранить' }}
</button>
</div>
</div>
</article>
</div>
</section>
</template>