299 lines
12 KiB
Vue
299 lines
12 KiB
Vue
<script setup lang="ts">
|
||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||
import {
|
||
MeDocument,
|
||
ReferralStatsDocument,
|
||
RequestRewardWithdrawalDocument,
|
||
} from '~/composables/graphql/generated';
|
||
|
||
const route = useRoute();
|
||
|
||
const meQuery = useQuery(MeDocument);
|
||
const referralStatsQuery = useQuery(ReferralStatsDocument);
|
||
const requestWithdrawalMutation = useMutation(RequestRewardWithdrawalDocument, { throws: 'never' });
|
||
|
||
const withdrawalAmount = ref('');
|
||
const withdrawalFeedback = ref('');
|
||
const withdrawalFeedbackTone = ref<'success' | 'error'>('success');
|
||
|
||
const bonusAccount = computed(() => referralStatsQuery.result.value?.referralStats ?? null);
|
||
const me = computed(() => meQuery.result.value?.me ?? null);
|
||
const transactions = computed(() => bonusAccount.value?.transactions ?? []);
|
||
const pendingWithdrawals = computed(() => bonusAccount.value?.pendingWithdrawals ?? []);
|
||
const availableBalance = computed(() => bonusAccount.value?.availableBalance ?? 0);
|
||
const canWithdraw = computed(() => availableBalance.value >= 100);
|
||
const selectedEntry = computed(() => String(route.query.entry || '').trim());
|
||
|
||
const entryTitle = computed(() => {
|
||
if (selectedEntry.value.includes('withdrawal')) {
|
||
return 'Вы открыли бонусную программу из уведомления о выводе.';
|
||
}
|
||
if (selectedEntry.value.includes('balance')) {
|
||
return 'Вы открыли бонусную программу из уведомления об изменении баланса.';
|
||
}
|
||
if (selectedEntry.value) {
|
||
return 'Вы открыли бонусную программу из специального перехода.';
|
||
}
|
||
return 'Отдельный бонусный интерфейс для клиента.';
|
||
});
|
||
|
||
const suggestedWithdrawalAmount = computed(() => {
|
||
if (availableBalance.value < 100) {
|
||
return '';
|
||
}
|
||
|
||
return String(Math.floor(availableBalance.value));
|
||
});
|
||
|
||
watch(
|
||
suggestedWithdrawalAmount,
|
||
(value) => {
|
||
if (!withdrawalAmount.value && value) {
|
||
withdrawalAmount.value = value;
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
function formatMoney(value: number) {
|
||
return new Intl.NumberFormat('ru-RU', {
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 2,
|
||
}).format(value);
|
||
}
|
||
|
||
function formatDate(value: string) {
|
||
return new Date(value).toLocaleString('ru-RU');
|
||
}
|
||
|
||
async function submitWithdrawal() {
|
||
withdrawalFeedback.value = '';
|
||
|
||
const amount = Number(withdrawalAmount.value);
|
||
if (!Number.isFinite(amount) || amount < 100) {
|
||
withdrawalFeedbackTone.value = 'error';
|
||
withdrawalFeedback.value = 'Минимальная сумма вывода - 100.';
|
||
return;
|
||
}
|
||
|
||
if (amount > availableBalance.value) {
|
||
withdrawalFeedbackTone.value = 'error';
|
||
withdrawalFeedback.value = 'Сумма вывода не может быть больше доступного баланса.';
|
||
return;
|
||
}
|
||
|
||
const response = await requestWithdrawalMutation.mutate({
|
||
input: {
|
||
amount,
|
||
},
|
||
});
|
||
|
||
const payload = response?.data?.requestRewardWithdrawal;
|
||
if (!payload) {
|
||
withdrawalFeedbackTone.value = 'error';
|
||
withdrawalFeedback.value = requestWithdrawalMutation.error.value?.message || 'Не удалось отправить заявку на вывод.';
|
||
return;
|
||
}
|
||
|
||
withdrawalFeedbackTone.value = 'success';
|
||
withdrawalFeedback.value = `Заявка на вывод создана: ${formatMoney(payload.amount)}.`;
|
||
withdrawalAmount.value = '';
|
||
await referralStatsQuery.refetch();
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="bonus-program-page space-y-8">
|
||
<div class="bonus-program-orbit bonus-program-orbit--a" aria-hidden="true" />
|
||
<div class="bonus-program-orbit bonus-program-orbit--b" aria-hidden="true" />
|
||
|
||
<header class="bonus-program-hero">
|
||
<div class="space-y-3">
|
||
<p class="bonus-program-kicker">Bonus Program</p>
|
||
<h1 class="bonus-program-title">
|
||
Чёрный кабинет бонусной программы
|
||
</h1>
|
||
<p class="bonus-program-copy">
|
||
{{ entryTitle }}
|
||
Здесь отдельно живут баланс, начисления, выводы и переходы из бонусных уведомлений.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap gap-3">
|
||
<NuxtLink to="/admin/settings/messages" class="bonus-program-ghost-button">
|
||
Message board
|
||
</NuxtLink>
|
||
<NuxtLink to="/notifications" class="bonus-program-ghost-button">
|
||
История уведомлений
|
||
</NuxtLink>
|
||
</div>
|
||
</header>
|
||
|
||
<div v-if="referralStatsQuery.loading.value || meQuery.loading.value" class="bonus-program-panel">
|
||
Загружаем бонусную программу...
|
||
</div>
|
||
|
||
<template v-else>
|
||
<section class="grid gap-4 xl:grid-cols-[1.3fr_0.9fr]">
|
||
<article class="bonus-program-panel">
|
||
<p class="bonus-program-caption">Аккаунт</p>
|
||
<div class="mt-4 flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||
<div class="space-y-2">
|
||
<h2 class="text-4xl font-black tracking-[-0.05em] text-white">
|
||
{{ me?.fullName || 'Клиент бонусной программы' }}
|
||
</h2>
|
||
<p class="text-sm leading-6 text-white/65">
|
||
Отдельная зона для бонусных начислений и статусов выводов.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="space-y-2 text-left lg:text-right">
|
||
<p class="bonus-program-caption">Доступный баланс</p>
|
||
<p class="text-5xl font-black tracking-[-0.05em] text-white">
|
||
{{ formatMoney(availableBalance) }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 grid gap-3 md:grid-cols-3">
|
||
<div class="bonus-program-stat">
|
||
<span class="bonus-program-stat__label">Рефералы</span>
|
||
<span class="bonus-program-stat__value">{{ bonusAccount?.referralsCount ?? 0 }}</span>
|
||
</div>
|
||
<div class="bonus-program-stat">
|
||
<span class="bonus-program-stat__label">Начисления</span>
|
||
<span class="bonus-program-stat__value">{{ transactions.length }}</span>
|
||
</div>
|
||
<div class="bonus-program-stat">
|
||
<span class="bonus-program-stat__label">Активные выводы</span>
|
||
<span class="bonus-program-stat__value">{{ pendingWithdrawals.length }}</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="bonus-program-panel">
|
||
<p class="bonus-program-caption">Вывод бонусов</p>
|
||
<div class="mt-4 space-y-4">
|
||
<p class="text-sm leading-6 text-white/70">
|
||
При изменении статуса вывода клиент получает отдельное уведомление и возвращается именно в этот экран.
|
||
</p>
|
||
|
||
<label class="block space-y-2">
|
||
<span class="text-xs font-semibold uppercase tracking-[0.14em] text-white/55">Сумма заявки</span>
|
||
<input
|
||
v-model="withdrawalAmount"
|
||
type="number"
|
||
min="100"
|
||
step="1"
|
||
class="bonus-program-input"
|
||
placeholder="Например, 1500"
|
||
>
|
||
</label>
|
||
|
||
<button
|
||
class="bonus-program-primary-button"
|
||
:disabled="requestWithdrawalMutation.loading.value || !canWithdraw"
|
||
@click="submitWithdrawal"
|
||
>
|
||
{{
|
||
requestWithdrawalMutation.loading.value
|
||
? 'Отправляем заявку...'
|
||
: canWithdraw
|
||
? 'Подать заявку на вывод'
|
||
: 'Недостаточно бонусов для вывода'
|
||
}}
|
||
</button>
|
||
|
||
<div
|
||
v-if="withdrawalFeedback"
|
||
class="rounded-[24px] border px-4 py-3 text-sm"
|
||
:class="withdrawalFeedbackTone === 'success' ? 'border-[#184e31] bg-[#0f1f16] text-[#dff7e8]' : 'border-[#6a2626] bg-[#1b1010] text-[#ffd4d4]'"
|
||
>
|
||
{{ withdrawalFeedback }}
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="grid gap-4 xl:grid-cols-[1.05fr_0.95fr]">
|
||
<article class="bonus-program-panel">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<div>
|
||
<p class="bonus-program-caption">Начисления</p>
|
||
<h2 class="mt-2 text-2xl font-black tracking-[-0.04em] text-white">История бонусов</h2>
|
||
</div>
|
||
<span class="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em] text-white/55">
|
||
{{ transactions.length }}
|
||
</span>
|
||
</div>
|
||
|
||
<div v-if="transactions.length === 0" class="bonus-program-empty mt-5">
|
||
Пока нет начислений. Когда придут первые бонусы, они появятся здесь отдельной чёрной лентой.
|
||
</div>
|
||
|
||
<div v-else class="mt-5 space-y-3">
|
||
<article
|
||
v-for="transaction in transactions"
|
||
:key="transaction.id"
|
||
class="bonus-program-feed-item"
|
||
>
|
||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||
<div class="space-y-2">
|
||
<p class="text-lg font-bold text-white">+{{ formatMoney(transaction.amount) }}</p>
|
||
<p class="text-sm leading-6 text-white/72">{{ transaction.reason }}</p>
|
||
<p class="text-xs uppercase tracking-[0.12em] text-white/40">{{ formatDate(transaction.createdAt) }}</p>
|
||
</div>
|
||
|
||
<NuxtLink
|
||
v-if="transaction.orderId"
|
||
to="/orders"
|
||
class="bonus-program-inline-link"
|
||
>
|
||
Открыть заказы
|
||
</NuxtLink>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="bonus-program-panel">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<div>
|
||
<p class="bonus-program-caption">Выводы</p>
|
||
<h2 class="mt-2 text-2xl font-black tracking-[-0.04em] text-white">Текущий статус заявок</h2>
|
||
</div>
|
||
<span class="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em] text-white/55">
|
||
{{ pendingWithdrawals.length }}
|
||
</span>
|
||
</div>
|
||
|
||
<div v-if="pendingWithdrawals.length === 0" class="bonus-program-empty mt-5">
|
||
Активных выводов сейчас нет. Как только менеджер получит заявку, она появится в этом блоке.
|
||
</div>
|
||
|
||
<div v-else class="mt-5 space-y-3">
|
||
<article
|
||
v-for="withdrawal in pendingWithdrawals"
|
||
:key="withdrawal.id"
|
||
class="bonus-program-feed-item"
|
||
>
|
||
<div class="space-y-2">
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<p class="text-lg font-bold text-white">{{ formatMoney(withdrawal.amount) }}</p>
|
||
<span class="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em] text-white/55">
|
||
{{ withdrawal.status }}
|
||
</span>
|
||
</div>
|
||
<p class="text-xs uppercase tracking-[0.12em] text-white/40">{{ formatDate(withdrawal.createdAt) }}</p>
|
||
<p v-if="withdrawal.reviewComment" class="text-sm leading-6 text-white/72">
|
||
{{ withdrawal.reviewComment }}
|
||
</p>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
</template>
|
||
</section>
|
||
</template>
|