Add client referral bonus manager flow

This commit is contained in:
Ruslan Bakiev
2026-04-04 14:59:02 +07:00
parent ad1f6b8a35
commit 540418c1dc
7 changed files with 262 additions and 15 deletions

View File

@@ -92,6 +92,12 @@ const filteredWithdrawals = computed(() => {
title="Бонусы"
:search-placeholder="activeTab === 'balances' ? 'Пользователь, email или сумма' : 'Пользователь, сумма или статус'"
>
<template #controls>
<NuxtLink to="/bonus-system/referrals/new" class="btn btn-primary border-0">
Добавить связь
</NuxtLink>
</template>
<template #tabs>
<div class="tabs tabs-boxed w-fit bg-white">
<button

View File

@@ -1,24 +1,81 @@
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable';
import { CreateReferralDocument } from '~/composables/graphql/generated';
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
CreateReferralDocument,
ManagerReferralLinksDocument,
ManagerUsersDocument,
type ManagerReferralLinksQuery,
type ManagerUsersQuery,
} from '~/composables/graphql/generated';
definePageMeta({
middleware: ['manager-only'],
});
type ManagerUserItem = ManagerUsersQuery['managerUsers'][number];
type ManagerReferralLinkItem = ManagerReferralLinksQuery['managerReferralLinks'][number];
const referrerUserId = ref('');
const refereeUserId = ref('');
const bonusPercent = ref(5);
const createdReferralId = ref('');
const errorMessage = ref('');
const usersQuery = useQuery(ManagerUsersDocument);
const linksQuery = useQuery(ManagerReferralLinksDocument);
const createReferralMutation = useMutation(CreateReferralDocument);
const clientOptions = computed<ManagerUserItem[]>(() => (
(usersQuery.result.value?.managerUsers ?? [])
.filter((user) => user.role === 'CLIENT')
.slice()
.sort((left, right) => left.fullName.localeCompare(right.fullName, 'ru'))
));
const referralLinks = computed<ManagerReferralLinkItem[]>(() => (
linksQuery.result.value?.managerReferralLinks ?? []
));
function userOptionLabel(user: ManagerUserItem) {
return [user.fullName, user.companyName || user.email]
.filter(Boolean)
.join(' • ');
}
async function createReferral() {
createdReferralId.value = '';
const response = await createReferralMutation.mutate({
input: {
refereeUserId: refereeUserId.value,
},
});
errorMessage.value = '';
createdReferralId.value = response?.data?.createReferral.id ?? '';
if (!referrerUserId.value || !refereeUserId.value) {
errorMessage.value = 'Выберите обоих клиентов для связки.';
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;
}
try {
const response = await createReferralMutation.mutate({
input: {
referrerUserId: referrerUserId.value,
refereeUserId: refereeUserId.value,
bonusPercent: normalizedBonusPercent,
},
});
createdReferralId.value = response?.data?.createReferral.id ?? '';
refereeUserId.value = '';
await linksQuery.refetch();
} catch (error: any) {
errorMessage.value = error?.message || 'Не удалось создать бонусную связку.';
}
}
</script>
@@ -28,22 +85,96 @@ async function createReferral() {
<div class="manager-hero">
<p class="manager-eyebrow">Бонусы</p>
<h1 class="manager-title">Создать реферальную связь</h1>
<h1 class="manager-title">Создать бонусную связку клиентов</h1>
<p class="max-w-2xl text-sm text-[#466653]">
Первый клиент получает процент бонуса, когда заказ второго клиента переходит в статус доставленного.
</p>
</div>
<div class="surface-card rounded-3xl p-5">
<div class="surface-card rounded-3xl p-5 space-y-4">
<label class="form-control">
<span class="label-text">ID приглашенного пользователя</span>
<input v-model="refereeUserId" class="input manager-field w-full" placeholder="user id">
<span class="label-text">Клиент, который получает бонус</span>
<select v-model="referrerUserId" class="select manager-field w-full">
<option value="">Выберите клиента</option>
<option v-for="user in clientOptions" :key="user.id" :value="user.id">
{{ userOptionLabel(user) }}
</option>
</select>
</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 clientOptions" :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">
<button class="btn btn-primary border-0" @click="createReferral">Создать</button>
<button
class="btn btn-primary border-0"
:disabled="createReferralMutation.loading.value || usersQuery.loading.value"
@click="createReferral"
>
{{ createReferralMutation.loading.value ? 'Сохраняем...' : 'Создать связь' }}
</button>
</div>
</div>
<div v-if="errorMessage" class="surface-card rounded-3xl border border-[#d27d7d] bg-[#fff4f4] p-5 text-sm text-[#8b2a2a]">
{{ errorMessage }}
</div>
<div v-if="createdReferralId" class="surface-card rounded-3xl p-5 text-sm text-[#123824]">
Создана связь: <span class="font-semibold">{{ createdReferralId }}</span>
</div>
<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>
</template>