Files
web-frontend/app/pages/cart.vue
2026-04-04 09:08:51 +07:00

254 lines
8.5 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 {
MyDeliveryAddressesDocument,
SubmitCalculationOrderDocument,
type MyDeliveryAddressesQuery,
} from '~/composables/graphql/generated';
import { useClientCart } from '~/composables/useClientCart';
import { useCounterpartyProfile } from '~/composables/useCounterpartyProfile';
type DeliveryAddressItem = MyDeliveryAddressesQuery['myDeliveryAddresses'][number];
const { isComplete: isCounterpartyComplete, loading: counterpartyLoading } = useCounterpartyProfile();
const submitMutation = useMutation(SubmitCalculationOrderDocument, { throws: 'never' });
const deliveryAddressesQuery = useQuery(MyDeliveryAddressesDocument);
const {
items: cartItems,
fetchCart,
selectedDeliveryAddressId,
totalPositions,
totalItems,
totalVolume,
incrementQuantity,
decrementQuantity,
removeProduct,
clearCart,
setDeliveryAddress,
} = useClientCart();
const sending = ref(false);
const success = ref('');
const errorMessage = ref('');
const deliveryAddresses = computed<DeliveryAddressItem[]>(() => deliveryAddressesQuery.result.value?.myDeliveryAddresses ?? []);
const hasDeliveryAddresses = computed(() => deliveryAddresses.value.length > 0);
watch(
deliveryAddresses,
async (addresses) => {
if (addresses.length < 1) {
if (selectedDeliveryAddressId.value) {
await setDeliveryAddress(null);
}
return;
}
const hasCurrentSelection = addresses.some((address) => address.id === selectedDeliveryAddressId.value);
if (hasCurrentSelection) {
return;
}
const defaultAddress = addresses.find((address) => address.isDefault);
await setDeliveryAddress(defaultAddress?.id || addresses[0]?.id || null);
},
{ immediate: true },
);
onMounted(() => {
void fetchCart(true);
});
function lineVolume(productId: string) {
const item = cartItems.value.find((entry) => entry.productId === productId);
if (!item) {
return 0;
}
return Number(item.quantity) * Number(item.parameters.width) * Number(item.parameters.thickness);
}
function increment(productId: string) {
success.value = '';
errorMessage.value = '';
void incrementQuantity(productId);
}
function decrement(productId: string) {
success.value = '';
errorMessage.value = '';
void decrementQuantity(productId);
}
function removeFromCart(productId: string) {
success.value = '';
errorMessage.value = '';
void removeProduct(productId);
}
function selectDeliveryAddress(addressId: string) {
success.value = '';
errorMessage.value = '';
void setDeliveryAddress(addressId);
}
async function submitCart() {
success.value = '';
errorMessage.value = '';
if (!isCounterpartyComplete.value) {
errorMessage.value = 'Сначала заполните карточку контрагента в профиле.';
return;
}
if (!selectedDeliveryAddressId.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.parameters.width),
thickness: Number(item.parameters.thickness),
color: item.parameters.color,
},
deliveryAddressId: selectedDeliveryAddressId.value,
},
});
const code = result?.data?.submitCalculationOrder?.code;
if (!code) {
errorMessage.value = submitMutation.error.value?.message || 'Не удалось отправить одну из позиций.';
sending.value = false;
return;
}
createdCodes.push(code);
}
sending.value = false;
await clearCart();
success.value = `Отправлено заявок: ${createdCodes.length}. Статус: уточнение цены.`;
}
</script>
<template>
<section class="space-y-6">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Корзина</h1>
<div v-if="counterpartyLoading" class="alert surface-card">
Проверяем карточку контрагента...
</div>
<div v-else-if="!isCounterpartyComplete" class="alert alert-warning">
Для оформления заявки заполните карточку контрагента в
<NuxtLink to="/profile/counterparty" class="link link-hover font-semibold">профиле</NuxtLink>.
</div>
<div class="surface-card rounded-3xl p-4 md:p-5">
<h2 class="text-lg font-bold text-[#123824]">Адрес доставки</h2>
<div v-if="deliveryAddressesQuery.loading.value" class="alert mt-3 surface-card">
Загружаем адреса...
</div>
<div v-else-if="!hasDeliveryAddresses" class="alert alert-warning mt-3">
Адреса не добавлены.
<NuxtLink to="/profile/addresses" class="link link-hover font-semibold">Добавить адрес в профиле</NuxtLink>
</div>
<div v-else class="mt-3 space-y-2">
<label
v-for="address in deliveryAddresses"
:key="address.id"
class="flex cursor-pointer items-start gap-3 rounded-2xl bg-white p-3 transition hover:shadow-md"
>
<input
type="radio"
name="delivery-address"
class="radio radio-success mt-1"
:checked="selectedDeliveryAddressId === address.id"
@change="selectDeliveryAddress(address.id)"
>
<span>
<span class="block font-semibold text-[#123824]">
{{ address.label || 'Адрес доставки' }}
<span v-if="address.isDefault" class="badge badge-success ml-2">Основной</span>
</span>
<span class="block text-sm text-[#355947]">{{ address.unrestrictedValue || address.address }}</span>
</span>
</label>
</div>
</div>
<h2 class="text-xl font-bold text-[#123824]">Позиции</h2>
<div v-if="cartItems.length === 0" class="alert surface-card">
Корзина пока пустая. Добавьте товар из каталога.
</div>
<ul v-else class="space-y-3">
<li
v-for="item in cartItems"
:key="item.productId"
class="surface-card flex flex-col gap-3 rounded-3xl px-4 py-4 md:flex-row md:items-center md:justify-between md:px-5 md:py-5"
>
<div>
<p class="font-semibold text-[#123824]">{{ item.productName }}</p>
<p class="text-xs opacity-70">SKU: {{ item.sku }}</p>
<p class="text-sm opacity-80">
Объем: {{ lineVolume(item.productId) }}
</p>
</div>
<div class="flex items-center gap-2">
<button class="btn btn-square btn-sm" @click="decrement(item.productId)">-</button>
<span class="min-w-8 text-center font-semibold">{{ item.quantity }}</span>
<button class="btn btn-square btn-sm" @click="increment(item.productId)">+</button>
<button class="btn btn-ghost btn-sm text-error" @click="removeFromCart(item.productId)">
Удалить
</button>
</div>
</li>
</ul>
<div class="divider my-1" />
<div class="space-y-2 text-sm text-[#214735]">
<div class="flex items-center justify-between">
<span>Позиций</span>
<span class="font-semibold">{{ totalPositions }}</span>
</div>
<div class="flex items-center justify-between">
<span>Количество, шт.</span>
<span class="font-semibold">{{ totalItems }}</span>
</div>
<div class="flex items-center justify-between">
<span>Суммарный объем</span>
<span class="font-semibold">{{ totalVolume }}</span>
</div>
</div>
<button
class="btn w-full bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sending || counterpartyLoading || !isCounterpartyComplete || !selectedDeliveryAddressId || cartItems.length === 0"
@click="submitCart"
>
{{ sending ? 'Отправляем…' : 'Оформить заявку' }}
</button>
<div v-if="success" class="alert alert-success">{{ success }}</div>
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
</section>
</template>