Add message board and bonus program preview
This commit is contained in:
298
app/pages/bonus-program.vue
Normal file
298
app/pages/bonus-program.vue
Normal 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>
|
||||
Reference in New Issue
Block a user