Add delivery addresses to profile, cart, and orders

This commit is contained in:
Ruslan Bakiev
2026-04-03 10:26:47 +07:00
parent 0aad9177f8
commit 4c82c2437a
11 changed files with 757 additions and 41 deletions

View File

@@ -1,11 +1,19 @@
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable';
import { SubmitCalculationOrderDocument } from '~/composables/graphql/generated';
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,
totalPositions,
@@ -16,10 +24,34 @@ const {
removeProduct,
clearCart,
} = useClientCart();
const selectedDeliveryAddressId = ref('');
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,
(addresses) => {
if (addresses.length < 1) {
selectedDeliveryAddressId.value = '';
return;
}
const hasCurrentSelection = addresses.some((address) => address.id === selectedDeliveryAddressId.value);
if (hasCurrentSelection) {
return;
}
const defaultAddress = addresses.find((address) => address.isDefault);
selectedDeliveryAddressId.value = defaultAddress?.id || addresses[0]?.id || '';
},
{ immediate: true },
);
function lineVolume(productId: string) {
const item = cartItems.value.find((entry) => entry.productId === productId);
if (!item) {
@@ -56,6 +88,11 @@ async function submitCart() {
return;
}
if (!selectedDeliveryAddressId.value) {
errorMessage.value = 'Выберите адрес доставки в профиле.';
return;
}
if (cartItems.value.length < 1) {
errorMessage.value = 'Добавьте хотя бы одну позицию в корзину.';
return;
@@ -74,6 +111,7 @@ async function submitCart() {
thickness: Number(item.parameters.thickness),
color: item.parameters.color,
},
deliveryAddressId: selectedDeliveryAddressId.value,
},
});
@@ -97,7 +135,7 @@ async function submitCart() {
<section class="space-y-6">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Корзина</h1>
<div v-if="counterpartyLoading.value" class="alert surface-card border-0">
<div v-if="counterpartyLoading" class="alert surface-card border-0">
Проверяем карточку контрагента...
</div>
<div v-else-if="!isCounterpartyComplete" class="alert alert-warning">
@@ -105,6 +143,40 @@ async function submitCart() {
<NuxtLink to="/profile" 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" class="alert mt-3 surface-card border-0">
Загружаем адреса...
</div>
<div v-else-if="!hasDeliveryAddresses" class="alert alert-warning mt-3">
Адреса не добавлены.
<NuxtLink to="/profile" 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 border border-[#d6ebde] bg-white/75 p-3"
>
<input
v-model="selectedDeliveryAddressId"
type="radio"
name="delivery-address"
class="radio radio-success mt-1"
:value="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 border-0">
@@ -155,7 +227,7 @@ async function submitCart() {
<button
class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sending || counterpartyLoading.value || !isCounterpartyComplete || cartItems.length === 0"
:disabled="sending || counterpartyLoading || !isCounterpartyComplete || !selectedDeliveryAddressId || cartItems.length === 0"
@click="submitCart"
>
{{ sending ? 'Отправляем…' : 'Оформить заявку' }}

View File

@@ -133,6 +133,10 @@ const filteredOrders = computed(() => {
</div>
</div>
<div class="mt-3 rounded-xl border border-[#d6ebde] bg-white/75 px-3 py-2 text-sm text-[#214735]">
Адрес доставки: {{ order.deliveryAddress || 'адрес не был выбран' }}
</div>
<div v-if="order.status === 'WAITING_DOUBLE_CONFIRM'" class="mt-4 flex flex-wrap gap-2">
<button class="btn btn-sm border-0 bg-[#139957] text-white hover:bg-[#0d854a]" @click="approve(order.id)">
Подтвердить

View File

@@ -1,10 +1,15 @@
<script setup lang="ts">
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
CreateMyDeliveryAddressDocument,
DeleteMyDeliveryAddressDocument,
MeDocument,
MyCounterpartyProfileDocument,
MyDeliveryAddressesDocument,
MyMessengerConnectionsDocument,
SetMyDefaultDeliveryAddressDocument,
UpsertMyCounterpartyProfileDocument,
type MyDeliveryAddressesQuery,
} from '~/composables/graphql/generated';
import { buildMessengerBotStartUrl } from '~/composables/useMessengerBotLink';
import { isCounterpartyProfileComplete } from '~/composables/useCounterpartyProfile';
@@ -39,6 +44,16 @@ type BankSuggestion = {
};
};
type AddressSuggestion = {
value: string;
unrestricted_value?: string;
data?: {
fias_id?: string;
};
};
type DeliveryAddressItem = MyDeliveryAddressesQuery['myDeliveryAddresses'][number];
const config = useRuntimeConfig();
const counterpartyForm = reactive({
@@ -59,12 +74,18 @@ const counterpartyForm = reactive({
const profileFeedback = ref('');
const profileFeedbackTone = ref<'success' | 'error'>('success');
const addressFeedback = ref('');
const addressFeedbackTone = ref<'success' | 'error'>('success');
const meQuery = useQuery(MeDocument);
const profileQuery = useQuery(MyCounterpartyProfileDocument);
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
const deliveryAddressesQuery = useQuery(MyDeliveryAddressesDocument);
const saveCounterpartyMutation = useMutation(UpsertMyCounterpartyProfileDocument, { throws: 'never' });
const createAddressMutation = useMutation(CreateMyDeliveryAddressDocument, { throws: 'never' });
const setDefaultAddressMutation = useMutation(SetMyDefaultDeliveryAddressDocument, { throws: 'never' });
const deleteAddressMutation = useMutation(DeleteMyDeliveryAddressDocument, { throws: 'never' });
const companySearch = ref('');
const partySuggestions = ref<PartySuggestion[]>([]);
@@ -78,8 +99,26 @@ const bankLoading = ref(false);
const bankOpen = ref(false);
const bankSearchTimer = ref<ReturnType<typeof setTimeout> | null>(null);
const addressForm = reactive({
label: '',
address: '',
unrestrictedValue: '',
fiasId: '',
});
const addressSearch = ref('');
const addressSuggestions = ref<AddressSuggestion[]>([]);
const addressLoading = ref(false);
const addressOpen = ref(false);
const addressSearchTimer = ref<ReturnType<typeof setTimeout> | null>(null);
const addressBusyId = ref<string | null>(null);
const partyDropdownRef = ref<HTMLElement | null>(null);
const bankDropdownRef = ref<HTMLElement | null>(null);
const addressDropdownRef = ref<HTMLElement | null>(null);
const counterpartySectionRef = ref<HTMLElement | null>(null);
const notificationsSectionRef = ref<HTMLElement | null>(null);
const addressesSectionRef = ref<HTMLElement | null>(null);
watch(
() => profileQuery.result.value?.myCounterpartyProfile,
@@ -123,6 +162,30 @@ const maxConnection = computed(() =>
),
);
const connectedMessengerCount = computed(() => {
let count = 0;
if (telegramConnection.value) {
count += 1;
}
if (maxConnection.value) {
count += 1;
}
return count;
});
const notificationsSummary = computed(() => {
if (connectedMessengerCount.value === 0) {
return 'Уведомления не подключены. Настройте Telegram или Max, чтобы не пропускать обновления заказа.';
}
if (connectedMessengerCount.value === 1) {
return 'Подключён 1 канал уведомлений. Рекомендуем подключить второй как резерв.';
}
return 'Подключены оба канала уведомлений. Вы будете получать обновления по заказам в мессенджерах.';
});
const deliveryAddresses = computed<DeliveryAddressItem[]>(() => deliveryAddressesQuery.result.value?.myDeliveryAddresses ?? []);
const defaultDeliveryAddress = computed(() => deliveryAddresses.value.find((item) => item.isDefault) ?? null);
function buildBotConnectUrl(baseUrl: string) {
const accountEmail = meQuery.result.value?.me?.email?.trim().toLowerCase();
if (!accountEmail || !baseUrl) {
@@ -151,6 +214,14 @@ function clearBankTimer() {
bankSearchTimer.value = null;
}
function clearAddressTimer() {
if (!addressSearchTimer.value) {
return;
}
clearTimeout(addressSearchTimer.value);
addressSearchTimer.value = null;
}
async function fetchPartySuggestions() {
const query = companySearch.value.trim();
if (query.length < 2) {
@@ -195,6 +266,28 @@ async function fetchBankSuggestions() {
});
}
async function fetchAddressSuggestions() {
const query = addressSearch.value.trim();
if (query.length < 2) {
addressSuggestions.value = [];
addressOpen.value = false;
return;
}
addressLoading.value = true;
await $fetch<{ suggestions: AddressSuggestion[] }>('/api/dadata/address', {
method: 'POST',
body: { query },
})
.then((response) => {
addressSuggestions.value = response.suggestions || [];
addressOpen.value = addressSuggestions.value.length > 0;
})
.finally(() => {
addressLoading.value = false;
});
}
function schedulePartySuggest() {
clearPartyTimer();
partySearchTimer.value = setTimeout(() => {
@@ -209,6 +302,13 @@ function scheduleBankSuggest() {
}, 250);
}
function scheduleAddressSuggest() {
clearAddressTimer();
addressSearchTimer.value = setTimeout(() => {
void fetchAddressSuggestions();
}, 250);
}
function applyPartySuggestion(item: PartySuggestion) {
partyOpen.value = false;
companySearch.value = item.value;
@@ -237,6 +337,25 @@ function applyBankSuggestion(item: BankSuggestion) {
counterpartyForm.correspondentAccount = item.data?.correspondent_account || '';
}
function applyAddressSuggestion(item: AddressSuggestion) {
addressOpen.value = false;
addressSearch.value = item.value;
addressForm.address = item.value;
addressForm.unrestrictedValue = item.unrestricted_value || item.value;
addressForm.fiasId = item.data?.fias_id || '';
}
function scrollToSection(section: 'counterparty' | 'notifications' | 'addresses') {
const element =
section === 'counterparty'
? counterpartySectionRef.value
: section === 'notifications'
? notificationsSectionRef.value
: addressesSectionRef.value;
element?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function closeDropdownsFromOutside(event: MouseEvent) {
const target = event.target as Node | null;
if (partyDropdownRef.value && target && !partyDropdownRef.value.contains(target)) {
@@ -245,6 +364,9 @@ function closeDropdownsFromOutside(event: MouseEvent) {
if (bankDropdownRef.value && target && !bankDropdownRef.value.contains(target)) {
bankOpen.value = false;
}
if (addressDropdownRef.value && target && !addressDropdownRef.value.contains(target)) {
addressOpen.value = false;
}
}
async function saveCounterpartyProfile() {
@@ -279,6 +401,83 @@ async function saveCounterpartyProfile() {
await profileQuery.refetch();
}
async function addDeliveryAddress() {
addressFeedback.value = '';
const normalizedAddress = addressForm.address.trim() || addressSearch.value.trim();
if (normalizedAddress.length < 5) {
addressFeedbackTone.value = 'error';
addressFeedback.value = 'Введите адрес через подсказки DaData.';
return;
}
const result = await createAddressMutation.mutate({
input: {
label: addressForm.label.trim() ? addressForm.label.trim() : null,
address: normalizedAddress,
unrestrictedValue: addressForm.unrestrictedValue.trim() ? addressForm.unrestrictedValue.trim() : null,
fiasId: addressForm.fiasId.trim() ? addressForm.fiasId.trim() : null,
},
});
const payload = result?.data?.createMyDeliveryAddress;
if (!payload) {
addressFeedbackTone.value = 'error';
addressFeedback.value = createAddressMutation.error.value?.message || 'Не удалось добавить адрес.';
return;
}
addressForm.label = '';
addressForm.address = '';
addressForm.unrestrictedValue = '';
addressForm.fiasId = '';
addressSearch.value = '';
addressSuggestions.value = [];
addressOpen.value = false;
addressFeedbackTone.value = 'success';
addressFeedback.value = 'Адрес сохранён.';
await deliveryAddressesQuery.refetch();
}
async function setDefaultAddress(addressId: string) {
addressFeedback.value = '';
addressBusyId.value = addressId;
const result = await setDefaultAddressMutation.mutate({ addressId });
const payload = result?.data?.setMyDefaultDeliveryAddress;
addressBusyId.value = null;
if (!payload) {
addressFeedbackTone.value = 'error';
addressFeedback.value = setDefaultAddressMutation.error.value?.message || 'Не удалось сделать адрес основным.';
return;
}
addressFeedbackTone.value = 'success';
addressFeedback.value = 'Основной адрес обновлён.';
await deliveryAddressesQuery.refetch();
}
async function deleteAddress(addressId: string) {
addressFeedback.value = '';
addressBusyId.value = addressId;
const result = await deleteAddressMutation.mutate({ addressId });
const payload = result?.data?.deleteMyDeliveryAddress;
addressBusyId.value = null;
if (!payload) {
addressFeedbackTone.value = 'error';
addressFeedback.value = deleteAddressMutation.error.value?.message || 'Не удалось удалить адрес.';
return;
}
addressFeedbackTone.value = 'success';
addressFeedback.value = 'Адрес удалён.';
await deliveryAddressesQuery.refetch();
}
onMounted(() => {
document.addEventListener('click', closeDropdownsFromOutside);
});
@@ -287,6 +486,7 @@ onBeforeUnmount(() => {
document.removeEventListener('click', closeDropdownsFromOutside);
clearPartyTimer();
clearBankTimer();
clearAddressTimer();
});
</script>
@@ -294,8 +494,48 @@ onBeforeUnmount(() => {
<section class="space-y-6">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Профиль</h1>
<div class="grid gap-4 lg:grid-cols-[1.8fr_1fr]">
<div class="surface-card rounded-3xl p-5">
<div class="grid gap-4 md:grid-cols-3">
<article
class="rounded-3xl border p-4"
:class="profileIsComplete ? 'border-[#76c893] bg-[#ecfdf3]' : 'border-[#f5d49a] bg-[#fff9eb]'"
>
<p class="text-sm opacity-70">Карточка контрагента</p>
<p class="mt-2 text-lg font-bold text-[#123824]">
{{ profileIsComplete ? 'Заполнена' : 'Требует внимания' }}
</p>
<p class="mt-2 text-sm text-[#355947]">
{{
profileIsComplete
? 'Можно оформлять заказы и отправлять заявки менеджеру.'
: 'Заполните реквизиты, чтобы открыть оформление заявок.'
}}
</p>
<button class="btn btn-sm mt-4 w-full" @click="scrollToSection('counterparty')">Открыть карточку</button>
</article>
<article class="rounded-3xl border border-[#d6ebde] bg-[#f7fffb] p-4">
<p class="text-sm opacity-70">Уведомления</p>
<p class="mt-2 text-lg font-bold text-[#123824]">{{ connectedMessengerCount }}/2 каналов подключено</p>
<p class="mt-2 text-sm text-[#355947]">{{ notificationsSummary }}</p>
<button class="btn btn-sm mt-4 w-full" @click="scrollToSection('notifications')">Открыть уведомления</button>
</article>
<article class="rounded-3xl border border-[#d6ebde] bg-[#f7fffb] p-4">
<p class="text-sm opacity-70">Адреса доставки</p>
<p class="mt-2 text-lg font-bold text-[#123824]">{{ deliveryAddresses.length }}</p>
<p class="mt-2 text-sm text-[#355947]">
{{
defaultDeliveryAddress
? `Основной: ${defaultDeliveryAddress.label || defaultDeliveryAddress.address}`
: 'Добавьте хотя бы один адрес для оформления заявок.'
}}
</p>
<button class="btn btn-sm mt-4 w-full" @click="scrollToSection('addresses')">Открыть адреса</button>
</article>
</div>
<div class="grid gap-4 lg:grid-cols-[1.7fr_1fr]">
<div ref="counterpartySectionRef" class="surface-card rounded-3xl p-5">
<div class="space-y-1">
<h2 class="text-xl font-bold text-[#123824]">Карточка контрагента</h2>
<p class="text-sm text-[#355947]">
@@ -305,7 +545,7 @@ onBeforeUnmount(() => {
<div class="mt-4 space-y-4">
<section>
<h3 class="mb-3 text-base font-bold">1. Контрагент (Dadata)</h3>
<h3 class="mb-3 text-base font-bold">1. Контрагент (DaData)</h3>
<div ref="partyDropdownRef" class="relative">
<fieldset class="fieldset">
@@ -377,7 +617,7 @@ onBeforeUnmount(() => {
<div class="divider my-0" />
<section>
<h3 class="mb-3 text-base font-bold">2. Банк (Dadata)</h3>
<h3 class="mb-3 text-base font-bold">2. Банк (DaData)</h3>
<div ref="bankDropdownRef" class="relative">
<fieldset class="fieldset">
@@ -473,42 +713,136 @@ onBeforeUnmount(() => {
</div>
</div>
<aside class="surface-card rounded-3xl p-5">
<h2 class="text-xl font-bold text-[#123824]">Уведомления</h2>
<div class="mt-4 space-y-3">
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
<p class="font-semibold">Telegram</p>
<p class="text-sm opacity-80">
{{ telegramConnection ? `Подключен: ${telegramConnection.channelId}` : 'Не подключен' }}
</p>
<a
:href="telegramConnectUrl || undefined"
target="_blank"
rel="noopener noreferrer"
class="btn btn-secondary mt-3 w-full"
:class="{ 'btn-disabled pointer-events-none': !telegramConnectUrl }"
<div class="space-y-4">
<aside ref="notificationsSectionRef" class="surface-card rounded-3xl p-5">
<h2 class="text-xl font-bold text-[#123824]">Уведомления</h2>
<p class="mt-2 text-sm text-[#355947]">{{ notificationsSummary }}</p>
<div class="mt-4 space-y-3">
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
<p class="font-semibold">Telegram</p>
<p class="text-sm opacity-80">
{{ telegramConnection ? `Подключен: ${telegramConnection.channelId}` : 'Не подключен' }}
</p>
<a
:href="telegramConnectUrl || undefined"
target="_blank"
rel="noopener noreferrer"
class="btn btn-secondary mt-3 w-full"
:class="{ 'btn-disabled pointer-events-none': !telegramConnectUrl }"
>
{{ telegramConnection ? 'Переподключить Telegram' : 'Подключить Telegram' }}
</a>
</div>
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
<p class="font-semibold">Max</p>
<p class="text-sm opacity-80">
{{ maxConnection ? `Подключен: ${maxConnection.channelId}` : 'Не подключен' }}
</p>
<a
:href="maxConnectUrl || undefined"
target="_blank"
rel="noopener noreferrer"
class="btn btn-accent mt-3 w-full"
:class="{ 'btn-disabled pointer-events-none': !maxConnectUrl }"
>
{{ maxConnection ? 'Переподключить Max' : 'Подключить Max' }}
</a>
</div>
</div>
</aside>
<aside ref="addressesSectionRef" class="surface-card rounded-3xl p-5">
<h2 class="text-xl font-bold text-[#123824]">Адреса доставки</h2>
<p class="mt-2 text-sm text-[#355947]">
Добавьте адрес через DaData и выберите основной. Этот адрес будет подставляться при оформлении заявки из корзины.
</p>
<fieldset class="fieldset mt-4">
<legend class="fieldset-legend">Название адреса (необязательно)</legend>
<input v-model="addressForm.label" type="text" class="input input-bordered w-full" placeholder="Склад МСК" >
</fieldset>
<div ref="addressDropdownRef" class="relative mt-2">
<fieldset class="fieldset">
<legend class="fieldset-legend">Поиск адреса (DaData)</legend>
<input
v-model="addressSearch"
type="text"
class="input input-bordered w-full"
placeholder="Начните вводить адрес"
@input="scheduleAddressSuggest"
@focus="addressOpen = addressSuggestions.length > 0"
>
</fieldset>
<span v-if="addressLoading" class="loading loading-spinner loading-sm absolute right-3 top-1/2 -translate-y-1/2" />
<div
v-if="addressOpen && addressSuggestions.length > 0"
class="absolute z-30 mt-2 max-h-72 w-full overflow-auto rounded-box border border-base-300 bg-base-100 p-2 shadow-xl"
>
{{ telegramConnection ? 'Переподключить Telegram' : 'Подключить Telegram' }}
</a>
<button
v-for="item in addressSuggestions"
:key="`${item.value}-${item.data?.fias_id || ''}`"
type="button"
class="btn btn-ghost mb-1 h-auto min-h-0 w-full justify-start whitespace-normal px-3 py-2 text-left"
@click="applyAddressSuggestion(item)"
>
<span class="block text-sm font-semibold">{{ item.value }}</span>
</button>
</div>
</div>
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
<p class="font-semibold">Max</p>
<p class="text-sm opacity-80">
{{ maxConnection ? `Подключен: ${maxConnection.channelId}` : 'Не подключен' }}
</p>
<a
:href="maxConnectUrl || undefined"
target="_blank"
rel="noopener noreferrer"
class="btn btn-accent mt-3 w-full"
:class="{ 'btn-disabled pointer-events-none': !maxConnectUrl }"
>
{{ maxConnection ? 'Переподключить Max' : 'Подключить Max' }}
</a>
<button class="btn btn-primary mt-4 w-full" :disabled="createAddressMutation.loading.value" @click="addDeliveryAddress">
{{ createAddressMutation.loading.value ? 'Добавляем' : 'Добавить адрес' }}
</button>
<div v-if="addressFeedback" class="alert mt-3" :class="addressFeedbackTone === 'success' ? 'alert-success' : 'alert-error'">
{{ addressFeedback }}
</div>
</div>
</aside>
<div class="mt-4 space-y-2">
<div v-if="deliveryAddressesQuery.loading.value" class="alert surface-card border-0">Загрузка адресов...</div>
<div v-else-if="deliveryAddresses.length === 0" class="alert surface-card border-0">
Пока нет адресов доставки. Добавьте первый адрес через DaData.
</div>
<article
v-for="address in deliveryAddresses"
:key="address.id"
class="rounded-2xl border border-[#d6ebde] bg-white/75 p-3"
>
<div class="flex items-start justify-between gap-2">
<div>
<p class="font-semibold text-[#123824]">{{ address.label || 'Адрес доставки' }}</p>
<p class="text-sm text-[#355947]">{{ address.unrestrictedValue || address.address }}</p>
</div>
<span v-if="address.isDefault" class="badge badge-success">Основной</span>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button
v-if="!address.isDefault"
class="btn btn-sm"
:disabled="addressBusyId === address.id"
@click="setDefaultAddress(address.id)"
>
{{ addressBusyId === address.id ? 'Сохраняем…' : 'Сделать основным' }}
</button>
<button
class="btn btn-sm btn-ghost text-error"
:disabled="addressBusyId === address.id"
@click="deleteAddress(address.id)"
>
{{ addressBusyId === address.id ? 'Удаляем…' : 'Удалить' }}
</button>
</div>
</article>
</div>
</aside>
</div>
</div>
</section>
</template>