Simplify bonus account link flow

This commit is contained in:
Ruslan Bakiev
2026-04-07 14:44:52 +07:00
parent af5d06f990
commit 345301e138
2 changed files with 16 additions and 173 deletions

View File

@@ -2,11 +2,9 @@
import { useQuery } from '@vue/apollo-composable'; import { useQuery } from '@vue/apollo-composable';
import { import {
ManagerBonusBalancesDocument, ManagerBonusBalancesDocument,
ManagerReferralLinksDocument,
ManagerUsersDocument, ManagerUsersDocument,
ManagerWithdrawalRequestsDocument, ManagerWithdrawalRequestsDocument,
type ManagerBonusBalancesQuery, type ManagerBonusBalancesQuery,
type ManagerReferralLinksQuery,
type ManagerUsersQuery, type ManagerUsersQuery,
type ManagerWithdrawalRequestsQuery, type ManagerWithdrawalRequestsQuery,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
@@ -19,7 +17,6 @@ definePageMeta({
}); });
type BalanceItem = ManagerBonusBalancesQuery['managerBonusBalances'][number]; type BalanceItem = ManagerBonusBalancesQuery['managerBonusBalances'][number];
type ReferralLinkItem = ManagerReferralLinksQuery['managerReferralLinks'][number];
type ManagerUserItem = ManagerUsersQuery['managerUsers'][number]; type ManagerUserItem = ManagerUsersQuery['managerUsers'][number];
type WithdrawalItem = ManagerWithdrawalRequestsQuery['managerWithdrawalRequests'][number]; type WithdrawalItem = ManagerWithdrawalRequestsQuery['managerWithdrawalRequests'][number];
type ProductCard = { type ProductCard = {
@@ -33,7 +30,6 @@ type ProductCard = {
const route = useRoute(); const route = useRoute();
const search = ref(''); const search = ref('');
const balancesQuery = useQuery(ManagerBonusBalancesDocument); const balancesQuery = useQuery(ManagerBonusBalancesDocument);
const referralLinksQuery = useQuery(ManagerReferralLinksDocument);
const usersQuery = useQuery(ManagerUsersDocument); const usersQuery = useQuery(ManagerUsersDocument);
const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, { const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, {
status: 'PENDING', status: 'PENDING',
@@ -95,34 +91,15 @@ const productCards: ProductCard[] = [
]; ];
const balances = computed<BalanceItem[]>(() => balancesQuery.result.value?.managerBonusBalances ?? []); const balances = computed<BalanceItem[]>(() => balancesQuery.result.value?.managerBonusBalances ?? []);
const referralLinks = computed<ReferralLinkItem[]>(() => referralLinksQuery.result.value?.managerReferralLinks ?? []);
const users = computed<ManagerUserItem[]>(() => usersQuery.result.value?.managerUsers ?? []); const users = computed<ManagerUserItem[]>(() => usersQuery.result.value?.managerUsers ?? []);
const withdrawals = computed<WithdrawalItem[]>(() => withdrawalsQuery.result.value?.managerWithdrawalRequests ?? []); const withdrawals = computed<WithdrawalItem[]>(() => withdrawalsQuery.result.value?.managerWithdrawalRequests ?? []);
const usersById = computed(() => new Map(users.value.map((user) => [user.id, user]))); const usersById = computed(() => new Map(users.value.map((user) => [user.id, user])));
const referralLinksByReferrer = computed(() => {
const grouped = new Map<string, ReferralLinkItem[]>();
for (const link of referralLinks.value) {
const existing = grouped.get(link.referrerId) ?? [];
existing.push(link);
grouped.set(link.referrerId, existing);
}
return grouped;
});
const filteredBalances = computed(() => { const filteredBalances = computed(() => {
const query = search.value.trim().toLowerCase(); const query = search.value.trim().toLowerCase();
return balances.value return balances.value
.filter((item) => { .filter((item) => {
const links = referralLinksByReferrer.value.get(item.userId);
if (!links?.length) {
return false;
}
if (!query) { if (!query) {
return true; return true;
} }
@@ -132,22 +109,11 @@ const filteredBalances = computed(() => {
item.email, item.email,
item.companyName || '', item.companyName || '',
String(item.balance), String(item.balance),
...links.flatMap((link) => [ String(item.transactionsCount),
link.refereeName,
link.refereeEmail,
link.refereeCompanyName || '',
String(link.bonusPercent),
]),
] ]
.join(' ') .join(' ')
.toLowerCase() .toLowerCase()
.includes(query); .includes(query);
})
.slice()
.sort((left, right) => {
const leftLatest = referralLinksByReferrer.value.get(left.userId)?.[0]?.createdAt ?? '';
const rightLatest = referralLinksByReferrer.value.get(right.userId)?.[0]?.createdAt ?? '';
return rightLatest.localeCompare(leftLatest);
}); });
}); });
@@ -321,7 +287,7 @@ function compactProductTitle(product: ProductCard) {
</UiSectionSearchHero> </UiSectionSearchHero>
<template v-if="activeTab === 'balances'"> <template v-if="activeTab === 'balances'">
<div v-if="balancesQuery.loading.value || referralLinksQuery.loading.value || usersQuery.loading.value" class="manager-empty-state"> <div v-if="balancesQuery.loading.value || usersQuery.loading.value" class="manager-empty-state">
Загружаем бонусные счета... Загружаем бонусные счета...
</div> </div>
<div v-else-if="filteredBalances.length === 0" class="manager-empty-state"> <div v-else-if="filteredBalances.length === 0" class="manager-empty-state">

View File

@@ -2,11 +2,8 @@
import { useMutation, useQuery } from '@vue/apollo-composable'; import { useMutation, useQuery } from '@vue/apollo-composable';
import { import {
CreateBonusProgramLinkDocument, CreateBonusProgramLinkDocument,
CreateReferralDocument,
ManagerReferralLinksDocument,
ManagerUsersDocument, ManagerUsersDocument,
type CreateBonusProgramLinkMutation, type CreateBonusProgramLinkMutation,
type ManagerReferralLinksQuery,
type ManagerUsersQuery, type ManagerUsersQuery,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
@@ -17,18 +14,11 @@ definePageMeta({
}); });
type ManagerUserItem = ManagerUsersQuery['managerUsers'][number]; type ManagerUserItem = ManagerUsersQuery['managerUsers'][number];
type ManagerReferralLinkItem = ManagerReferralLinksQuery['managerReferralLinks'][number]; const userId = ref('');
const referrerUserId = ref('');
const refereeUserId = ref('');
const bonusPercent = ref(5);
const createdReferralId = ref('');
const errorMessage = ref(''); const errorMessage = ref('');
const bonusProgramLink = ref(''); const bonusProgramLink = ref('');
const bonusProgramLinkExpiresAt = ref(''); const bonusProgramLinkExpiresAt = ref('');
const usersQuery = useQuery(ManagerUsersDocument); const usersQuery = useQuery(ManagerUsersDocument);
const linksQuery = useQuery(ManagerReferralLinksDocument);
const createReferralMutation = useMutation(CreateReferralDocument, { throws: 'never' });
const createBonusProgramLinkMutation = useMutation(CreateBonusProgramLinkDocument, { throws: 'never' }); const createBonusProgramLinkMutation = useMutation(CreateBonusProgramLinkDocument, { throws: 'never' });
const clientOptions = computed<ManagerUserItem[]>(() => ( const clientOptions = computed<ManagerUserItem[]>(() => (
@@ -36,87 +26,34 @@ const clientOptions = computed<ManagerUserItem[]>(() => (
.filter((user) => user.role === 'CLIENT') .filter((user) => user.role === 'CLIENT')
)); ));
const referrerOptions = computed<ManagerUserItem[]>(() => (
clientOptions.value.filter((user) => user.id !== refereeUserId.value)
));
const refereeOptions = computed<ManagerUserItem[]>(() => (
clientOptions.value.filter((user) => user.id !== referrerUserId.value)
));
const referralLinks = computed<ManagerReferralLinkItem[]>(() => (
linksQuery.result.value?.managerReferralLinks ?? []
));
watch(referrerUserId, (value) => {
if (value && value === refereeUserId.value) {
refereeUserId.value = '';
}
});
watch(refereeUserId, (value) => {
if (value && value === referrerUserId.value) {
referrerUserId.value = '';
}
});
function userOptionLabel(user: ManagerUserItem) { function userOptionLabel(user: ManagerUserItem) {
return [user.fullName, user.companyName || user.email] return [user.fullName, user.companyName || user.email]
.filter(Boolean) .filter(Boolean)
.join(' • '); .join(' • ');
} }
async function createReferral() { async function createBonusAccountLink() {
createdReferralId.value = '';
errorMessage.value = ''; errorMessage.value = '';
bonusProgramLink.value = ''; bonusProgramLink.value = '';
bonusProgramLinkExpiresAt.value = ''; bonusProgramLinkExpiresAt.value = '';
if (!referrerUserId.value || !refereeUserId.value) { if (!userId.value) {
errorMessage.value = 'Выберите обоих клиентов для связки.'; errorMessage.value = 'Выберите клиента.';
return; return;
} }
if (referrerUserId.value === refereeUserId.value) {
errorMessage.value = 'Нельзя связать клиента с самим собой.';
return;
}
const normalizedBonusPercent = Number(bonusPercent.value);
if (!Number.isFinite(normalizedBonusPercent) || normalizedBonusPercent <= 0 || normalizedBonusPercent > 100) {
errorMessage.value = 'Укажите процент бонуса от 0.01 до 100.';
return;
}
const response = await createReferralMutation.mutate({
input: {
referrerUserId: referrerUserId.value,
refereeUserId: refereeUserId.value,
bonusPercent: normalizedBonusPercent,
},
});
if (!response?.data?.createReferral.id) {
errorMessage.value = createReferralMutation.error.value?.message || 'Не удалось создать бонусную связку.';
return;
}
createdReferralId.value = response.data.createReferral.id;
const bonusLinkResponse = await createBonusProgramLinkMutation.mutate({ const bonusLinkResponse = await createBonusProgramLinkMutation.mutate({
userId: referrerUserId.value, userId: userId.value,
}); });
const bonusLinkPayload: CreateBonusProgramLinkMutation['createBonusProgramLink'] | undefined = bonusLinkResponse?.data?.createBonusProgramLink; const bonusLinkPayload: CreateBonusProgramLinkMutation['createBonusProgramLink'] | undefined = bonusLinkResponse?.data?.createBonusProgramLink;
if (!bonusLinkPayload?.url) { if (!bonusLinkPayload?.url) {
errorMessage.value = createBonusProgramLinkMutation.error.value?.message || 'Связка создана, но не удалось сгенерировать ссылку.'; errorMessage.value = createBonusProgramLinkMutation.error.value?.message || 'Не удалось сгенерировать ссылку.';
return; return;
} }
bonusProgramLink.value = bonusLinkPayload.url; bonusProgramLink.value = bonusLinkPayload.url;
bonusProgramLinkExpiresAt.value = bonusLinkPayload.expiresAt; bonusProgramLinkExpiresAt.value = bonusLinkPayload.expiresAt;
refereeUserId.value = '';
await linksQuery.refetch();
} }
async function copyBonusProgramLink() { async function copyBonusProgramLink() {
@@ -137,51 +74,28 @@ function formatDateTime(value: string) {
<UiBackHeader <UiBackHeader
to="/admin/bonuses/balances" to="/admin/bonuses/balances"
back-label="Назад к бонусным счетам" back-label="Назад к бонусным счетам"
title="Создать бонусную связку" title="Создать бонусный счет"
subtitle="После создания менеджер сразу получает ссылку, которую можно переслать клиенту." subtitle="Менеджер выбирает клиента и сразу получает ссылку, которую можно переслать ему."
/> />
<div class="surface-card rounded-3xl p-5 space-y-4"> <div class="surface-card rounded-3xl p-5 space-y-4">
<label class="form-control"> <label class="form-control">
<span class="label-text">Клиент, который получает бонус</span> <span class="label-text">Клиент</span>
<select v-model="referrerUserId" class="select manager-field w-full"> <select v-model="userId" class="select manager-field w-full">
<option value="">Выберите клиента</option> <option value="">Выберите клиента</option>
<option v-for="user in referrerOptions" :key="user.id" :value="user.id"> <option v-for="user in clientOptions" :key="user.id" :value="user.id">
{{ userOptionLabel(user) }} {{ userOptionLabel(user) }}
</option> </option>
</select> </select>
</label> </label>
<label class="form-control">
<span class="label-text">Клиент, с чьих заказов начисляется бонус</span>
<select v-model="refereeUserId" class="select manager-field w-full">
<option value="">Выберите клиента</option>
<option v-for="user in refereeOptions" :key="user.id" :value="user.id">
{{ userOptionLabel(user) }}
</option>
</select>
</label>
<label class="form-control">
<span class="label-text">Процент бонусной программы</span>
<input
v-model="bonusPercent"
type="number"
min="0.01"
max="100"
step="0.01"
class="input manager-field w-full"
placeholder="5"
>
</label>
<div class="mt-4"> <div class="mt-4">
<button <button
class="btn btn-primary border-0" class="btn btn-primary border-0"
:disabled="createReferralMutation.loading.value || usersQuery.loading.value" :disabled="createBonusProgramLinkMutation.loading.value || usersQuery.loading.value"
@click="createReferral" @click="createBonusAccountLink"
> >
{{ createReferralMutation.loading.value || createBonusProgramLinkMutation.loading.value ? 'Сохраняем...' : 'Создать связь' }} {{ createBonusProgramLinkMutation.loading.value ? 'Генерируем...' : 'Создать ссылку' }}
</button> </button>
</div> </div>
</div> </div>
@@ -190,10 +104,6 @@ function formatDateTime(value: string) {
{{ errorMessage }} {{ errorMessage }}
</div> </div>
<div v-if="createdReferralId" class="surface-card rounded-3xl p-5 text-sm text-[#123824]">
Создана связь: <span class="font-semibold">{{ createdReferralId }}</span>
</div>
<article v-if="bonusProgramLink" class="surface-card rounded-3xl p-5 space-y-4"> <article v-if="bonusProgramLink" class="surface-card rounded-3xl p-5 space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<p class="text-sm font-semibold text-[#123824]">Ссылка в бонусный кабинет</p> <p class="text-sm font-semibold text-[#123824]">Ссылка в бонусный кабинет</p>
@@ -225,38 +135,5 @@ function formatDateTime(value: string) {
</button> </button>
</div> </div>
</article> </article>
<div class="space-y-4">
<div class="flex items-center justify-between gap-3">
<h2 class="text-lg font-bold text-[#123824]">Текущие бонусные связки</h2>
<span class="text-sm text-[#466653]">{{ referralLinks.length }}</span>
</div>
<div v-if="linksQuery.loading.value" class="manager-empty-state">
Загружаем связки...
</div>
<div v-else-if="referralLinks.length === 0" class="manager-empty-state">
Бонусных связок пока нет.
</div>
<div v-else class="space-y-3">
<article
v-for="link in referralLinks"
:key="link.id"
class="surface-card rounded-3xl p-5"
>
<div class="space-y-2">
<p class="text-sm font-semibold text-[#123824]">
{{ link.referrerName }} получает {{ link.bonusPercent }}% с заказов {{ link.refereeName }}
</p>
<p class="text-sm text-[#466653]">
{{ link.referrerCompanyName || link.referrerEmail }} {{ link.refereeCompanyName || link.refereeEmail }}
</p>
<p class="text-xs text-[#5c7b69]">
Создано {{ new Date(link.createdAt).toLocaleString() }}
</p>
</div>
</article>
</div>
</div>
</section> </section>
</template> </template>