Files
web-frontend/app/pages/clients/index.vue
2026-04-04 09:48:43 +07:00

208 lines
6.1 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 { useQuery } from '@vue/apollo-composable';
import {
ManagerUsersDocument,
RegistrationRequestsDocument,
type ManagerUsersQuery,
type RegistrationRequestsQuery,
} from '~/composables/graphql/generated';
definePageMeta({
middleware: ['manager-only'],
});
type ManagerUserItem = ManagerUsersQuery['managerUsers'][number];
type RequestItem = RegistrationRequestsQuery['registrationRequests'][number];
const route = useRoute();
const router = useRouter();
const search = ref('');
const usersQuery = useQuery(ManagerUsersDocument);
const requestsQuery = useQuery(RegistrationRequestsDocument, {
status: null,
});
const activeTab = computed<'users' | 'requests'>(() => (
route.query.tab === 'requests' ? 'requests' : 'users'
));
function setTab(tab: 'users' | 'requests') {
void router.replace({
query: {
...route.query,
tab,
},
});
}
const filteredUsers = computed(() => {
const items = usersQuery.result.value?.managerUsers ?? [];
const query = search.value.trim().toLowerCase();
return items.filter((item) => {
if (!query) {
return true;
}
return [
item.fullName,
item.email,
item.companyName || '',
item.inn || '',
]
.join(' ')
.toLowerCase()
.includes(query);
});
});
function requestStatusLabel(status: RequestItem['status']) {
if (status === 'APPROVED') {
return 'Одобрена';
}
if (status === 'REJECTED') {
return 'Отклонена';
}
return 'На проверке';
}
function requestStatusClass(status: RequestItem['status']) {
if (status === 'APPROVED') {
return 'badge badge-success border-0';
}
if (status === 'REJECTED') {
return 'badge badge-error border-0';
}
return 'badge badge-warning border-0';
}
const filteredRequests = computed(() => {
const items = requestsQuery.result.value?.registrationRequests ?? [];
const query = search.value.trim().toLowerCase();
return items.filter((item) => {
if (!query) {
return true;
}
return [
item.companyName,
item.contactName,
item.email,
item.inn || '',
]
.join(' ')
.toLowerCase()
.includes(query);
});
});
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('');
}
</script>
<template>
<section class="space-y-6">
<UiSectionSearchHero
v-model="search"
title="Пользователи"
:search-placeholder="activeTab === 'users' ? 'Имя, email, компания или ИНН' : 'Компания, контакт, email или ИНН'"
>
<template #controls>
<NuxtLink to="/clients/invite" class="btn btn-primary border-0">
Пригласить
</NuxtLink>
</template>
</UiSectionSearchHero>
<div class="tabs tabs-boxed w-fit bg-white">
<button
class="tab"
:class="{ 'tab-active': activeTab === 'users' }"
@click="setTab('users')"
>
Пользователи
</button>
<button
class="tab"
:class="{ 'tab-active': activeTab === 'requests' }"
@click="setTab('requests')"
>
Заявки
</button>
</div>
<template v-if="activeTab === 'users'">
<div v-if="usersQuery.loading.value" class="manager-empty-state">
Загружаем пользователей...
</div>
<div v-else-if="filteredUsers.length === 0" class="manager-empty-state">
Пользователи по текущему запросу не найдены.
</div>
<div v-else class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-6">
<article
v-for="user in filteredUsers"
:key="user.id"
class="surface-card flex min-h-[260px] flex-col rounded-[32px] p-5"
>
<div class="mb-5 flex justify-center">
<div class="flex h-20 w-20 items-center justify-center rounded-[28px] bg-[linear-gradient(135deg,#dff7e9_0%,#c2ead3_100%)] text-2xl font-black text-[#123824] shadow-[inset_0_1px_0_rgba(255,255,255,0.65)]">
{{ userInitials(user.fullName) }}
</div>
</div>
<div class="mt-auto space-y-2 text-center">
<h2 class="text-lg font-bold leading-tight text-[#123824]">{{ user.fullName }}</h2>
<p class="text-sm text-[#466653] break-all">{{ user.email }}</p>
<p v-if="user.companyName" class="text-sm text-[#5c7b69]">{{ user.companyName }}</p>
<p v-else class="text-sm text-[#8ca896]">Компания не указана</p>
</div>
</article>
</div>
</template>
<template v-else>
<div v-if="requestsQuery.loading.value" class="manager-empty-state">
Загружаем заявки...
</div>
<div v-else-if="filteredRequests.length === 0" class="manager-empty-state">
Заявки по текущему запросу не найдены.
</div>
<div v-else class="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
<NuxtLink
v-for="request in filteredRequests"
:key="request.id"
:to="`/clients/${request.id}?tab=requests`"
class="surface-card rounded-3xl p-5"
>
<div class="flex items-start justify-between gap-3">
<div class="space-y-1">
<h2 class="text-lg font-bold text-[#123824]">{{ request.companyName }}</h2>
<p class="text-sm text-[#466653]">{{ request.contactName }}</p>
</div>
<span :class="requestStatusClass(request.status)">{{ requestStatusLabel(request.status) }}</span>
</div>
<div class="mt-4 space-y-2 text-sm text-[#355947]">
<p>{{ request.email }}</p>
<p v-if="request.inn">ИНН: {{ request.inn }}</p>
<p>{{ new Date(request.createdAt).toLocaleDateString() }}</p>
</div>
</NuxtLink>
</div>
</template>
</section>
</template>