Add message board and bonus program preview

This commit is contained in:
Ruslan Bakiev
2026-04-06 14:41:32 +07:00
parent 5396354962
commit ac5ee256fd
10 changed files with 1060 additions and 19 deletions

298
app/pages/bonus-program.vue Normal file
View File

@@ -0,0 +1,298 @@
<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="/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>