Simplify bonus account link flow
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user