263 lines
9.3 KiB
Vue
263 lines
9.3 KiB
Vue
<script setup lang="ts">
|
||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||
import {
|
||
CreateBonusProgramLinkDocument,
|
||
CreateReferralDocument,
|
||
ManagerReferralLinksDocument,
|
||
ManagerUsersDocument,
|
||
type CreateBonusProgramLinkMutation,
|
||
type ManagerReferralLinksQuery,
|
||
type ManagerUsersQuery,
|
||
} from '~/composables/graphql/generated';
|
||
|
||
definePageMeta({
|
||
middleware: ['manager-only'],
|
||
path: '/admin/bonuses/referrals/new',
|
||
alias: ['/bonus-system/referrals/new'],
|
||
});
|
||
|
||
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 bonusProgramLink = ref('');
|
||
const bonusProgramLinkExpiresAt = ref('');
|
||
const usersQuery = useQuery(ManagerUsersDocument);
|
||
const linksQuery = useQuery(ManagerReferralLinksDocument);
|
||
const createReferralMutation = useMutation(CreateReferralDocument, { throws: 'never' });
|
||
const createBonusProgramLinkMutation = useMutation(CreateBonusProgramLinkDocument, { throws: 'never' });
|
||
|
||
const clientOptions = computed<ManagerUserItem[]>(() => (
|
||
(usersQuery.result.value?.managerUsers ?? [])
|
||
.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) {
|
||
return [user.fullName, user.companyName || user.email]
|
||
.filter(Boolean)
|
||
.join(' • ');
|
||
}
|
||
|
||
async function createReferral() {
|
||
createdReferralId.value = '';
|
||
errorMessage.value = '';
|
||
bonusProgramLink.value = '';
|
||
bonusProgramLinkExpiresAt.value = '';
|
||
|
||
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;
|
||
}
|
||
|
||
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({
|
||
userId: referrerUserId.value,
|
||
});
|
||
|
||
const bonusLinkPayload: CreateBonusProgramLinkMutation['createBonusProgramLink'] | undefined = bonusLinkResponse?.data?.createBonusProgramLink;
|
||
if (!bonusLinkPayload?.url) {
|
||
errorMessage.value = createBonusProgramLinkMutation.error.value?.message || 'Связка создана, но не удалось сгенерировать ссылку.';
|
||
return;
|
||
}
|
||
|
||
bonusProgramLink.value = bonusLinkPayload.url;
|
||
bonusProgramLinkExpiresAt.value = bonusLinkPayload.expiresAt;
|
||
refereeUserId.value = '';
|
||
await linksQuery.refetch();
|
||
}
|
||
|
||
async function copyBonusProgramLink() {
|
||
if (!bonusProgramLink.value) {
|
||
return;
|
||
}
|
||
|
||
await navigator.clipboard.writeText(bonusProgramLink.value);
|
||
}
|
||
|
||
function formatDateTime(value: string) {
|
||
return new Date(value).toLocaleString('ru-RU');
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="space-y-6 max-w-3xl">
|
||
<UiBackHeader
|
||
to="/admin/bonuses/balances"
|
||
back-label="Назад к бонусным счетам"
|
||
title="Создать бонусную связку"
|
||
subtitle="После создания менеджер сразу получает ссылку, которую можно переслать клиенту."
|
||
/>
|
||
|
||
<div class="surface-card rounded-3xl p-5 space-y-4">
|
||
<label class="form-control">
|
||
<span class="label-text">Клиент, который получает бонус</span>
|
||
<select v-model="referrerUserId" class="select manager-field w-full">
|
||
<option value="">Выберите клиента</option>
|
||
<option v-for="user in referrerOptions" :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 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">
|
||
<button
|
||
class="btn btn-primary border-0"
|
||
:disabled="createReferralMutation.loading.value || usersQuery.loading.value"
|
||
@click="createReferral"
|
||
>
|
||
{{ createReferralMutation.loading.value || createBonusProgramLinkMutation.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>
|
||
|
||
<article v-if="bonusProgramLink" class="surface-card rounded-3xl p-5 space-y-4">
|
||
<div class="space-y-2">
|
||
<p class="text-sm font-semibold text-[#123824]">Ссылка в бонусный кабинет</p>
|
||
<p class="text-sm text-[#466653]">
|
||
Эту ссылку менеджер может сразу отправить клиенту.
|
||
</p>
|
||
<div class="rounded-[20px] bg-[#f8fbf9] px-4 py-3 text-sm font-semibold text-[#123824] break-all">
|
||
{{ bonusProgramLink }}
|
||
</div>
|
||
<p v-if="bonusProgramLinkExpiresAt" class="text-xs text-[#5c7b69]">
|
||
Действует до {{ formatDateTime(bonusProgramLinkExpiresAt) }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap gap-3">
|
||
<a
|
||
:href="bonusProgramLink"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
class="btn rounded-full border border-[#d7e9de] bg-white px-5 text-[#123824] hover:bg-[#f3f8f5]"
|
||
>
|
||
Открыть
|
||
</a>
|
||
<button
|
||
class="btn rounded-full border-0 bg-[#139957] px-5 text-white hover:bg-[#0d854a]"
|
||
@click="copyBonusProgramLink"
|
||
>
|
||
Скопировать
|
||
</button>
|
||
</div>
|
||
</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>
|
||
</template>
|