Refine cabinet layout, profile blocks, and list filters

This commit is contained in:
Ruslan Bakiev
2026-04-02 17:17:16 +07:00
parent d99e9345f2
commit caa9b9fba5
7 changed files with 372 additions and 277 deletions

View File

@@ -3,59 +3,123 @@ import { useMutation } from '@vue/apollo-composable';
import { SubmitCalculationOrderDocument } from '~/composables/graphql/generated';
import { useCounterpartyProfile } from '~/composables/useCounterpartyProfile';
const productName = ref('');
const quantity = ref(1);
const width = ref(100);
const thickness = ref(50);
const color = ref('прозрачный');
type CartLine = {
id: number;
productName: string;
quantity: number;
width: number;
thickness: number;
color: string;
};
const { mutate, loading, onDone, onError } = useMutation(SubmitCalculationOrderDocument);
const { isComplete: isCounterpartyComplete, loading: counterpartyLoading } = useCounterpartyProfile();
const submitMutation = useMutation(SubmitCalculationOrderDocument, { throws: 'never' });
const draft = reactive({
productName: '',
quantity: 1,
width: 100,
thickness: 50,
color: 'прозрачный',
});
const cartItems = ref<CartLine[]>([]);
const nextLineId = ref(1);
const sending = ref(false);
const success = ref('');
const errorMessage = ref('');
const calculatedVolume = computed(() => Number(quantity.value) * Number(width.value) * Number(thickness.value));
const { isComplete: isCounterpartyComplete, loading: counterpartyLoading } = useCounterpartyProfile();
onDone((result) => {
success.value = `Заявка ${result.data?.submitCalculationOrder.code} отправлена`;
errorMessage.value = '';
});
function lineVolume(item: CartLine) {
return Number(item.quantity) * Number(item.width) * Number(item.thickness);
}
onError((error) => {
errorMessage.value = error.message;
const totalItems = computed(() => cartItems.value.reduce((acc, item) => acc + Number(item.quantity), 0));
const totalVolume = computed(() => cartItems.value.reduce((acc, item) => acc + lineVolume(item), 0));
function addToCart() {
success.value = '';
});
errorMessage.value = '';
function submit() {
if (!isCounterpartyComplete.value) {
errorMessage.value = 'Сначала заполните карточку контрагента в профиле.';
success.value = '';
if (!draft.productName.trim()) {
errorMessage.value = 'Укажите название позиции.';
return;
}
mutate({
input: {
productName: productName.value,
quantity: Number(quantity.value),
parameters: {
width: Number(width.value),
thickness: Number(thickness.value),
color: color.value,
},
},
if (Number(draft.quantity) <= 0) {
errorMessage.value = 'Количество должно быть больше нуля.';
return;
}
cartItems.value.push({
id: nextLineId.value,
productName: draft.productName.trim(),
quantity: Number(draft.quantity),
width: Number(draft.width),
thickness: Number(draft.thickness),
color: draft.color.trim() || 'прозрачный',
});
nextLineId.value += 1;
draft.productName = '';
}
function removeFromCart(id: number) {
cartItems.value = cartItems.value.filter((item) => item.id !== id);
}
async function submitCart() {
success.value = '';
errorMessage.value = '';
if (!isCounterpartyComplete.value) {
errorMessage.value = 'Сначала заполните карточку контрагента в профиле.';
return;
}
if (cartItems.value.length < 1) {
errorMessage.value = 'Добавьте хотя бы одну позицию в корзину.';
return;
}
sending.value = true;
const createdCodes: string[] = [];
for (const item of cartItems.value) {
const result = await submitMutation.mutate({
input: {
productName: item.productName,
quantity: Number(item.quantity),
parameters: {
width: Number(item.width),
thickness: Number(item.thickness),
color: item.color,
},
},
});
const code = result?.data?.submitCalculationOrder?.code;
if (!code) {
errorMessage.value = submitMutation.error.value?.message || 'Не удалось отправить одну из позиций.';
sending.value = false;
return;
}
createdCodes.push(code);
}
sending.value = false;
cartItems.value = [];
success.value = `Отправлено заявок: ${createdCodes.length}.`;
}
</script>
<template>
<section class="space-y-6">
<div class="text-center">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Корзина и checkout</h1>
<p class="mt-1 text-sm text-[#28543f]/80">Заполните параметры и отправьте расчёт менеджеру.</p>
</div>
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Корзина</h1>
<div class="mx-auto max-w-4xl rounded-[30px] p-1 shadow-[0_26px_60px_rgba(13,133,74,0.18)]">
<div class="surface-card grid gap-6 rounded-[26px] p-5 md:grid-cols-[1.45fr_1fr] md:p-6">
<div class="space-y-3">
<div class="surface-card rounded-3xl p-5 md:p-6">
<div class="grid gap-6 xl:grid-cols-[1.6fr_1fr]">
<div class="space-y-4">
<div v-if="counterpartyLoading.value" class="alert">
Проверяем карточку контрагента...
</div>
@@ -64,57 +128,97 @@ function submit() {
<NuxtLink to="/profile" class="link link-hover font-semibold">профиле</NuxtLink>.
</div>
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">Название позиции</span>
<input v-model="productName" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
</label>
<div class="grid gap-3 sm:grid-cols-2">
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">Количество</span>
<input v-model="quantity" type="number" min="1" class="input input-bordered border-[#d0e8d8] bg-white/80">
</label>
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">Ширина</span>
<input v-model="width" type="number" min="1" class="input input-bordered border-[#d0e8d8] bg-white/80">
</label>
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">Толщина</span>
<input v-model="thickness" type="number" min="1" class="input input-bordered border-[#d0e8d8] bg-white/80">
</label>
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">Цвет</span>
<input v-model="color" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
</label>
<div class="rounded-2xl border border-base-300 bg-base-100 p-4">
<h2 class="text-lg font-bold text-[#123824]">Добавить позицию</h2>
<div class="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
<label class="form-control xl:col-span-3">
<span class="label-text">Название позиции</span>
<input v-model="draft.productName" type="text" class="input input-bordered w-full" placeholder="Лист ПЭТ" >
</label>
<label class="form-control">
<span class="label-text">Количество</span>
<input v-model="draft.quantity" type="number" min="1" class="input input-bordered w-full" >
</label>
<label class="form-control">
<span class="label-text">Ширина</span>
<input v-model="draft.width" type="number" min="1" class="input input-bordered w-full" >
</label>
<label class="form-control">
<span class="label-text">Толщина</span>
<input v-model="draft.thickness" type="number" min="1" class="input input-bordered w-full" >
</label>
<label class="form-control md:col-span-2 xl:col-span-3">
<span class="label-text">Цвет</span>
<input v-model="draft.color" type="text" class="input input-bordered w-full" >
</label>
</div>
<button class="btn btn-primary mt-4" @click="addToCart">Добавить в корзину</button>
</div>
<div class="rounded-2xl border border-base-300 bg-base-100 p-4">
<h2 class="text-lg font-bold text-[#123824]">Список позиций</h2>
<div v-if="cartItems.length === 0" class="alert mt-3">
Корзина пока пустая.
</div>
<ul v-else class="mt-3 space-y-2">
<li
v-for="item in cartItems"
:key="item.id"
class="flex flex-col gap-2 rounded-xl border border-[#d6ebde] bg-white/75 px-3 py-3 md:flex-row md:items-center md:justify-between"
>
<div>
<p class="font-semibold text-[#123824]">{{ item.productName }}</p>
<p class="text-sm opacity-80">
{{ item.quantity }} шт. {{ item.width }} × {{ item.thickness }} {{ item.color }}
</p>
</div>
<div class="flex items-center gap-3">
<span class="text-sm font-semibold text-[#123824]">Объем: {{ lineVolume(item) }}</span>
<button class="btn btn-ghost btn-sm text-error" @click="removeFromCart(item.id)">Удалить</button>
</div>
</li>
</ul>
</div>
<button class="btn border-0 bg-[#139957] text-white hover:bg-[#0d854a]" :disabled="loading || counterpartyLoading.value || !isCounterpartyComplete" @click="submit">
{{ loading ? 'Отправляем' : 'Отправить менеджеру' }}
</button>
</div>
<aside class="rounded-2xl border border-[#cde8d8] bg-white/75 p-4">
<h2 class="text-lg font-bold text-[#103221]">Итог checkout</h2>
<aside class="rounded-2xl border border-base-300 bg-base-100 p-4">
<h2 class="text-lg font-bold text-[#123824]">Итого</h2>
<ul class="mt-3 space-y-2 text-sm text-[#214735]">
<li class="flex items-center justify-between">
<span>Позиция</span>
<span class="font-semibold">{{ productName || 'Не указано' }}</span>
<span>Позиций</span>
<span class="font-semibold">{{ cartItems.length }}</span>
</li>
<li class="flex items-center justify-between">
<span>Количество</span>
<span class="font-semibold">{{ quantity }}</span>
<span>Количество, шт.</span>
<span class="font-semibold">{{ totalItems }}</span>
</li>
<li class="flex items-center justify-between">
<span>Цвет</span>
<span class="font-semibold">{{ color }}</span>
<span>Суммарный объем</span>
<span class="font-semibold">{{ totalVolume }}</span>
</li>
</ul>
<div class="mt-4 rounded-xl bg-[#139957]/10 p-3 text-sm text-[#174631]">
Оценочный объём: <span class="font-bold">{{ calculatedVolume }}</span>
</div>
<button
class="btn mt-4 w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sending || counterpartyLoading.value || !isCounterpartyComplete || cartItems.length === 0"
@click="submitCart"
>
{{ sending ? 'Отправляем…' : 'Оформить заявку' }}
</button>
</aside>
</div>
</div>
<div v-if="success" class="alert alert-success mx-auto max-w-4xl">{{ success }}</div>
<div v-if="success" class="alert alert-success">{{ success }}</div>
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
</section>
</template>