Files
web-frontend/app/pages/catalog-settings.vue
2026-04-09 16:03:32 +07:00

258 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>