Files
web-frontend/app/pages/clients/[id].vue
2026-04-06 15:23:11 +07:00

248 lines
9.6 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 {
ManagerOrdersDocument,
ManagerUsersDetailDocument,
RegistrationRequestsDocument,
ReviewRegistrationRequestDocument,
type ManagerOrdersQuery,
type ManagerUsersDetailQuery,
type RegistrationRequestsQuery,
} from '~/composables/graphql/generated';
import { messengerConnectionAvatarSrc } from '~/composables/useMessengerConnectionPresentation';
definePageMeta({
middleware: ['manager-only'],
});
type ManagerUserItem = ManagerUsersDetailQuery['managerUsers'][number];
type ManagerOrderItem = ManagerOrdersQuery['managerOrders'][number];
type RequestItem = RegistrationRequestsQuery['registrationRequests'][number];
const route = useRoute();
const entityId = computed(() => String(route.params.id || ''));
const isRequestMode = computed(() => route.query.tab === 'requests');
const backTarget = computed(() => '/clients');
const usersQuery = useQuery(ManagerUsersDetailDocument);
const requestsQuery = useQuery(RegistrationRequestsDocument, {
status: null,
});
const userOrdersQuery = useQuery(ManagerOrdersDocument, () => ({
status: null,
customerId: isRequestMode.value ? null : entityId.value,
}));
const reviewMutation = useMutation(ReviewRegistrationRequestDocument);
const currentUser = computed<ManagerUserItem | null>(() =>
(usersQuery.result.value?.managerUsers ?? []).find((item: ManagerUserItem) => item.id === entityId.value) ?? null,
);
const currentRequest = computed(() =>
(requestsQuery.result.value?.registrationRequests ?? []).find((item: RequestItem) => item.id === entityId.value),
);
const currentUserOrders = computed<ManagerOrderItem[]>(() => userOrdersQuery.result.value?.managerOrders ?? []);
const {
canLoadMore: canLoadMoreUserOrders,
loadMore: loadMoreUserOrders,
loadMoreSentinel: loadMoreUserOrdersSentinel,
remainingCount: remainingUserOrdersCount,
visibleItems: visibleUserOrders,
} = useIncrementalList(currentUserOrders, {
pageSize: 24,
enabled: computed(() => !isRequestMode.value),
resetKeys: [entityId, isRequestMode],
});
function userInitials(fullName: string) {
const parts = fullName
.trim()
.split(/\s+/)
.filter(Boolean)
.slice(0, 2);
if (!parts.length) {
return 'FR';
}
return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
}
async function approveRequest() {
if (!currentRequest.value) {
return;
}
await reviewMutation.mutate({
input: {
requestId: currentRequest.value.id,
decision: 'APPROVE',
},
});
await requestsQuery.refetch({ status: null });
}
async function rejectRequest() {
if (!currentRequest.value) {
return;
}
await reviewMutation.mutate({
input: {
requestId: currentRequest.value.id,
decision: 'REJECT',
rejectionReason: 'Не хватает данных для регистрации.',
},
});
await requestsQuery.refetch({ status: null });
}
</script>
<template>
<section class="space-y-6">
<NuxtLink :to="backTarget" class="text-sm font-semibold text-[#0d854a]"> Назад к пользователям</NuxtLink>
<template v-if="isRequestMode">
<div v-if="requestsQuery.loading.value" class="manager-empty-state">
Загружаем карточку клиента...
</div>
<div v-else-if="!currentRequest" class="manager-empty-state">
Карточка клиента не найдена.
</div>
<template v-else>
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div class="manager-hero">
<p class="manager-eyebrow">Заявка</p>
<h1 class="manager-title">{{ currentRequest.companyName }}</h1>
<p class="manager-copy">Контакт: {{ currentRequest.contactName }} · {{ currentRequest.email }}</p>
</div>
<div v-if="currentRequest.status === 'PENDING'" class="flex flex-wrap gap-2">
<button class="btn btn-success border-0" @click="approveRequest">Одобрить</button>
<button class="btn btn-error border-0" @click="rejectRequest">Отклонить</button>
</div>
</div>
<div class="grid gap-4 lg:grid-cols-3">
<div class="manager-stat-card">
<p class="manager-stat-label">Статус</p>
<p class="manager-stat-value text-lg">
{{ currentRequest.status === 'APPROVED' ? 'Активен' : currentRequest.status === 'REJECTED' ? 'Отклонен' : 'На проверке' }}
</p>
</div>
<div class="manager-stat-card">
<p class="manager-stat-label">Дата заявки</p>
<p class="manager-stat-value text-lg">{{ new Date(currentRequest.createdAt).toLocaleDateString() }}</p>
</div>
<div class="manager-stat-card">
<p class="manager-stat-label">ИНН</p>
<p class="manager-stat-value text-lg">{{ currentRequest.inn || 'Не указан' }}</p>
</div>
</div>
</template>
</template>
<template v-else>
<div v-if="usersQuery.loading.value || userOrdersQuery.loading.value" class="manager-empty-state">
Загружаем пользователя...
</div>
<div v-else-if="!currentUser" class="manager-empty-state">
Пользователь не найден.
</div>
<template v-else>
<div class="rounded-[36px] bg-[#edf3ee] p-6 md:p-8">
<div class="flex flex-col gap-6 md:flex-row md:items-start">
<div class="flex shrink-0 justify-center md:block">
<img
v-if="messengerConnectionAvatarSrc(currentUser.telegramConnection)"
:src="messengerConnectionAvatarSrc(currentUser.telegramConnection)"
:alt="currentUser.fullName"
class="h-28 w-28 rounded-[36px] object-cover shadow-[0_14px_30px_rgba(18,56,36,0.14)]"
>
<div
v-else
class="flex h-28 w-28 items-center justify-center rounded-[36px] bg-[linear-gradient(135deg,#dff7e9_0%,#c2ead3_100%)] text-4xl font-black text-[#123824] shadow-[0_14px_30px_rgba(18,56,36,0.14)]"
>
{{ userInitials(currentUser.fullName) }}
</div>
</div>
<div class="min-w-0 flex-1 space-y-5">
<div class="space-y-2">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Клиент</p>
<h1 class="text-3xl font-black tracking-[-0.03em] text-[#123824]">{{ currentUser.fullName }}</h1>
</div>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-[24px] bg-white/70 px-4 py-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Email</p>
<p class="mt-1 break-words text-sm font-semibold text-[#123824]">{{ currentUser.email }}</p>
</div>
<div class="rounded-[24px] bg-white/70 px-4 py-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Telegram</p>
<p class="mt-1 text-sm font-semibold text-[#123824]">
{{ currentUser.telegramConnection?.username ? `@${currentUser.telegramConnection.username}` : 'Не подключен' }}
</p>
</div>
<div class="rounded-[24px] bg-white/70 px-4 py-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Компания</p>
<p class="mt-1 text-sm font-semibold text-[#123824]">{{ currentUser.companyName || 'Не указана' }}</p>
</div>
<div class="rounded-[24px] bg-white/70 px-4 py-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">ИНН</p>
<p class="mt-1 text-sm font-semibold text-[#123824]">{{ currentUser.inn || 'Не указан' }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="space-y-4">
<div class="space-y-1">
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">Заказы пользователя</h2>
<p class="text-sm text-[#5c7b69]">
Всего заказов: {{ currentUser.orderCount }}
</p>
</div>
<div class="surface-card rounded-3xl p-5">
<div v-if="currentUserOrders.length === 0" class="manager-empty-state mt-4">
У пользователя пока нет заказов.
</div>
<div v-else class="mt-4 space-y-3">
<OrdersOrderSummaryCard
v-for="order in visibleUserOrders"
:key="order.id"
:to="`/client-orders/${order.id}`"
:code="order.code"
:status="order.status"
:created-at="order.createdAt"
:total-price="order.totalPrice"
:items="order.items"
/>
<div
v-if="canLoadMoreUserOrders"
ref="loadMoreUserOrdersSentinel"
class="flex justify-center pt-2"
>
<button class="btn btn-outline border-[#d7e9de] bg-white" @click="loadMoreUserOrders">
Показать ещё {{ Math.min(remainingUserOrdersCount, 24) }}
</button>
</div>
</div>
</div>
</div>
</template>
</template>
</section>
</template>