Add user and manager order detail pages
This commit is contained in:
@@ -1,58 +1,94 @@
|
||||
<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 requestId = computed(() => String(route.params.id || ''));
|
||||
const entityId = computed(() => String(route.params.id || ''));
|
||||
const isRequestMode = computed(() => route.query.tab === 'requests');
|
||||
const backTarget = computed(() => (
|
||||
route.query.tab === 'requests' ? '/clients?tab=requests' : '/clients'
|
||||
isRequestMode.value ? '/clients?tab=requests' : '/clients'
|
||||
));
|
||||
|
||||
const clientQuery = useQuery(RegistrationRequestsDocument, {
|
||||
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 currentClient = computed(() =>
|
||||
(clientQuery.result.value?.registrationRequests ?? []).find((item) => item.id === requestId.value),
|
||||
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 ?? []);
|
||||
|
||||
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 (!currentClient.value) {
|
||||
if (!currentRequest.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await reviewMutation.mutate({
|
||||
input: {
|
||||
requestId: currentClient.value.id,
|
||||
requestId: currentRequest.value.id,
|
||||
decision: 'APPROVE',
|
||||
},
|
||||
});
|
||||
|
||||
await clientQuery.refetch({ status: null });
|
||||
await requestsQuery.refetch({ status: null });
|
||||
}
|
||||
|
||||
async function rejectRequest() {
|
||||
if (!currentClient.value) {
|
||||
if (!currentRequest.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await reviewMutation.mutate({
|
||||
input: {
|
||||
requestId: currentClient.value.id,
|
||||
requestId: currentRequest.value.id,
|
||||
decision: 'REJECT',
|
||||
rejectionReason: 'Не хватает данных для регистрации.',
|
||||
},
|
||||
});
|
||||
|
||||
await clientQuery.refetch({ status: null });
|
||||
await requestsQuery.refetch({ status: null });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -60,71 +96,106 @@ async function rejectRequest() {
|
||||
<section class="space-y-6">
|
||||
<NuxtLink :to="backTarget" class="text-sm font-semibold text-[#0d854a]">← Назад к пользователям</NuxtLink>
|
||||
|
||||
<div v-if="clientQuery.loading.value" class="manager-empty-state">
|
||||
Загружаем карточку клиента...
|
||||
</div>
|
||||
<template v-if="isRequestMode">
|
||||
<div v-if="requestsQuery.loading.value" class="manager-empty-state">
|
||||
Загружаем карточку клиента...
|
||||
</div>
|
||||
|
||||
<div v-else-if="!currentClient" 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 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">{{ currentClient.companyName }}</h1>
|
||||
<p class="manager-copy">Контакт: {{ currentClient.contactName }} · {{ currentClient.email }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="currentClient.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 v-if="usersQuery.loading.value || userOrdersQuery.loading.value" class="manager-empty-state">
|
||||
Загружаем пользователя...
|
||||
</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">
|
||||
{{ currentClient.status === 'APPROVED' ? 'Активен' : currentClient.status === 'REJECTED' ? 'Отклонен' : 'На проверке' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="manager-stat-card">
|
||||
<p class="manager-stat-label">Дата заявки</p>
|
||||
<p class="manager-stat-value text-lg">{{ new Date(currentClient.createdAt).toLocaleDateString() }}</p>
|
||||
</div>
|
||||
<div class="manager-stat-card">
|
||||
<p class="manager-stat-label">ИНН</p>
|
||||
<p class="manager-stat-value text-lg">{{ currentClient.inn || 'Не указан' }}</p>
|
||||
</div>
|
||||
<div v-else-if="!currentUser" class="manager-empty-state">
|
||||
Пользователь не найден.
|
||||
</div>
|
||||
|
||||
<div class="surface-card rounded-3xl p-5">
|
||||
<h2 class="text-xl font-bold text-[#123824]">Информация</h2>
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
||||
<div class="manager-mini-card">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]">Компания</p>
|
||||
<p class="mt-2 text-sm text-[#123824]">{{ currentClient.companyName }}</p>
|
||||
<template v-else>
|
||||
<div class="surface-card flex flex-col gap-6 rounded-[36px] p-6 md:flex-row md:items-center">
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
v-if="messengerConnectionAvatarSrc(currentUser.telegramConnection)"
|
||||
:src="messengerConnectionAvatarSrc(currentUser.telegramConnection)"
|
||||
:alt="currentUser.fullName"
|
||||
class="h-28 w-28 rounded-[36px] object-cover shadow-[0_12px_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]"
|
||||
>
|
||||
{{ userInitials(currentUser.fullName) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="manager-mini-card">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]">Контакт</p>
|
||||
<p class="mt-2 text-sm text-[#123824]">{{ currentClient.contactName }}</p>
|
||||
</div>
|
||||
<div class="manager-mini-card">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]">Email</p>
|
||||
<p class="mt-2 text-sm text-[#123824]">{{ currentClient.email }}</p>
|
||||
</div>
|
||||
<div class="manager-mini-card">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]">Обновлено</p>
|
||||
<p class="mt-2 text-sm text-[#123824]">{{ new Date(currentClient.updatedAt).toLocaleString() }}</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="manager-eyebrow">Пользователь</p>
|
||||
<h1 class="manager-title">{{ currentUser.fullName }}</h1>
|
||||
<p class="text-sm text-[#466653]">{{ currentUser.email }}</p>
|
||||
<p v-if="currentUser.companyName" class="text-sm text-[#466653]">{{ currentUser.companyName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentClient.rejectionReason" class="surface-card rounded-3xl p-5">
|
||||
<h2 class="text-xl font-bold text-[#123824]">Причина отказа</h2>
|
||||
<p class="mt-3 text-sm text-[#a34a34]">{{ currentClient.rejectionReason }}</p>
|
||||
</div>
|
||||
<div class="surface-card rounded-3xl p-5">
|
||||
<h2 class="text-xl font-bold text-[#123824]">Заказы пользователя</h2>
|
||||
<div v-if="currentUserOrders.length === 0" class="manager-empty-state mt-4">
|
||||
У пользователя пока нет заказов.
|
||||
</div>
|
||||
<div v-else class="mt-4 space-y-3">
|
||||
<NuxtLink
|
||||
v-for="order in currentUserOrders"
|
||||
:key="order.id"
|
||||
:to="`/client-orders/${order.id}`"
|
||||
class="manager-mini-card block"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-semibold text-[#123824]">{{ order.code }}</p>
|
||||
<p class="text-sm text-[#355947]">{{ new Date(order.createdAt).toLocaleString() }}</p>
|
||||
<p v-if="order.deliveryAddress" class="text-sm text-[#355947]">{{ order.deliveryAddress }}</p>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-[#466653]">{{ order.status }}</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -147,9 +147,10 @@ function userInitials(fullName: string) {
|
||||
Пользователи по текущему запросу не найдены.
|
||||
</div>
|
||||
<div v-else class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-6">
|
||||
<article
|
||||
<NuxtLink
|
||||
v-for="user in filteredUsers"
|
||||
:key="user.id"
|
||||
:to="`/clients/${user.id}`"
|
||||
class="surface-card flex min-h-[280px] flex-col rounded-[32px] p-6"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
@@ -172,7 +173,7 @@ function userInitials(fullName: string) {
|
||||
<div class="pt-8 text-center">
|
||||
<h2 class="text-lg font-bold leading-tight text-[#123824]">{{ user.fullName }}</h2>
|
||||
</div>
|
||||
</article>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -69,10 +69,11 @@ const filteredOrders = computed(() => {
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<article
|
||||
<NuxtLink
|
||||
v-for="order in filteredOrders"
|
||||
:key="order.id"
|
||||
class="surface-card rounded-3xl p-4 md:p-5"
|
||||
:to="`/orders/${order.id}`"
|
||||
class="surface-card block rounded-3xl p-4 md:p-5"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
@@ -95,7 +96,7 @@ const filteredOrders = computed(() => {
|
||||
{{ item.productName }} × {{ item.quantity }}
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
76
app/pages/orders/[id].vue
Normal file
76
app/pages/orders/[id].vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@vue/apollo-composable';
|
||||
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
||||
import {
|
||||
MyOrdersDocument,
|
||||
type MyOrdersQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
|
||||
type OrderItem = MyOrdersQuery['myOrders'][number];
|
||||
|
||||
const route = useRoute();
|
||||
const orderId = computed(() => String(route.params.id || ''));
|
||||
const ordersQuery = useQuery(MyOrdersDocument);
|
||||
|
||||
const currentOrder = computed<OrderItem | null>(() =>
|
||||
(ordersQuery.result.value?.myOrders ?? []).find((item: OrderItem) => item.id === orderId.value) ?? null,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<NuxtLink to="/orders" class="text-sm font-semibold text-[#0d854a]">← Назад к моим заказам</NuxtLink>
|
||||
|
||||
<div v-if="ordersQuery.loading.value" class="manager-empty-state">
|
||||
Загружаем заказ...
|
||||
</div>
|
||||
|
||||
<div v-else-if="!currentOrder" class="manager-empty-state">
|
||||
Заказ не найден.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="manager-hero">
|
||||
<p class="manager-eyebrow">Заказ</p>
|
||||
<h1 class="manager-title">{{ currentOrder.code }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="surface-card rounded-3xl p-5">
|
||||
<h2 class="text-xl font-bold text-[#123824]">Статус заказа</h2>
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="space-y-1 text-sm text-[#355947]">
|
||||
<p>Создан: {{ new Date(currentOrder.createdAt).toLocaleString() }}</p>
|
||||
</div>
|
||||
<OrderStatusBadge :status="currentOrder.status" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="surface-card rounded-3xl p-5">
|
||||
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
|
||||
<ul class="mt-4 space-y-3">
|
||||
<li
|
||||
v-for="item in currentOrder.items"
|
||||
:key="item.id"
|
||||
class="manager-mini-card text-sm text-[#123824]"
|
||||
>
|
||||
{{ item.productName }} × {{ item.quantity }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="surface-card rounded-3xl p-5">
|
||||
<h2 class="text-xl font-bold text-[#123824]">Доставка</h2>
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
||||
<div class="manager-mini-card text-sm text-[#123824]">
|
||||
Адрес: {{ currentOrder.deliveryAddress || 'не выбран' }}
|
||||
</div>
|
||||
<div class="manager-mini-card text-sm text-[#123824]">
|
||||
Условия: {{ currentOrder.deliveryTerms || 'еще не указаны' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user