Simplify bonus manager cards
This commit is contained in:
@@ -14,19 +14,22 @@ type BonusCardLink = {
|
|||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
fullName: string;
|
fullName: string;
|
||||||
email: string;
|
email?: string;
|
||||||
companyName?: string | null;
|
companyName?: string | null;
|
||||||
balance: number;
|
balance: number;
|
||||||
|
avatarSrc?: string;
|
||||||
|
initials?: string;
|
||||||
|
compact?: boolean;
|
||||||
stats?: BonusCardStat[];
|
stats?: BonusCardStat[];
|
||||||
sourceLinks?: BonusCardLink[];
|
sourceLinks?: BonusCardLink[];
|
||||||
detailTo?: string;
|
|
||||||
detailLabel?: string;
|
|
||||||
}>(), {
|
}>(), {
|
||||||
|
email: '',
|
||||||
companyName: null,
|
companyName: null,
|
||||||
|
avatarSrc: '',
|
||||||
|
initials: 'FR',
|
||||||
|
compact: false,
|
||||||
stats: () => [],
|
stats: () => [],
|
||||||
sourceLinks: () => [],
|
sourceLinks: () => [],
|
||||||
detailTo: '',
|
|
||||||
detailLabel: 'Открыть',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatAmount(value: number) {
|
function formatAmount(value: number) {
|
||||||
@@ -38,58 +41,73 @@ function formatAmount(value: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article class="surface-card rounded-3xl p-5">
|
<article
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
class="surface-card rounded-[32px]"
|
||||||
|
:class="compact ? 'flex min-h-[280px] flex-col p-6' : 'p-6'"
|
||||||
|
>
|
||||||
|
<template v-if="compact">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img
|
||||||
|
v-if="avatarSrc"
|
||||||
|
:src="avatarSrc"
|
||||||
|
:alt="fullName"
|
||||||
|
class="h-24 w-24 rounded-[32px] object-cover shadow-[0_12px_30px_rgba(18,56,36,0.14)]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex h-24 w-24 items-center justify-center rounded-[32px] bg-[linear-gradient(135deg,#dff7e9_0%,#c2ead3_100%)] text-3xl font-black text-[#123824] shadow-[inset_0_1px_0_rgba(255,255,255,0.65)]"
|
||||||
|
>
|
||||||
|
{{ initials }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1" />
|
||||||
|
|
||||||
|
<div class="pt-8 text-center">
|
||||||
|
<h2 class="text-lg font-bold leading-tight text-[#123824]">{{ fullName }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-6 text-left">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Доступный бонус</p>
|
||||||
|
<p class="mt-2 text-2xl font-black leading-none text-[#123824]">{{ formatAmount(balance) }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h2 class="text-lg font-bold text-[#123824]">{{ fullName }}</h2>
|
<h1 class="text-2xl font-bold leading-tight text-[#123824]">{{ fullName }}</h1>
|
||||||
<p class="text-sm text-[#466653]">{{ email }}</p>
|
<p v-if="companyName || email" class="text-sm text-[#5c7b69]">
|
||||||
<p v-if="companyName" class="text-sm text-[#466653]">{{ companyName }}</p>
|
{{ companyName || email }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NuxtLink
|
<div class="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
v-if="detailTo"
|
<div
|
||||||
:to="detailTo"
|
v-for="stat in stats"
|
||||||
class="btn btn-accent btn-sm w-fit border-0"
|
:key="stat.label"
|
||||||
>
|
class="rounded-[24px] bg-[#f6fbf8] px-4 py-4"
|
||||||
{{ detailLabel }}
|
>
|
||||||
</NuxtLink>
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">{{ stat.label }}</p>
|
||||||
</div>
|
<p class="mt-2 text-xl font-bold leading-none text-[#123824]">{{ stat.value }}</p>
|
||||||
|
</div>
|
||||||
<div class="mt-5 rounded-[28px] bg-[linear-gradient(135deg,#123824_0%,#0d854a_100%)] px-5 py-4 text-white">
|
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/70">Доступный бонус</p>
|
|
||||||
<p class="mt-2 text-3xl font-black leading-none">{{ formatAmount(balance) }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl
|
|
||||||
v-if="stats.length"
|
|
||||||
class="mt-4 divide-y divide-[#deebe4] rounded-2xl border border-[#deebe4] bg-white"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="stat in stats"
|
|
||||||
:key="stat.label"
|
|
||||||
class="flex items-center justify-between gap-3 px-4 py-3 text-sm"
|
|
||||||
>
|
|
||||||
<dt class="text-[#5c7b69]">{{ stat.label }}</dt>
|
|
||||||
<dd class="text-right font-semibold text-[#123824]">{{ stat.value }}</dd>
|
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
|
||||||
|
|
||||||
<div class="mt-4 space-y-2 rounded-2xl bg-[#f4faf6] p-4 text-sm text-[#355947]">
|
<div class="mt-6 rounded-[24px] bg-[#f6fbf8] px-4 py-4">
|
||||||
<p class="font-semibold text-[#123824]">Начисление идёт с заказов:</p>
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Доступный бонус</p>
|
||||||
<div v-if="sourceLinks.length" class="space-y-2">
|
<p class="mt-2 text-3xl font-black leading-none text-[#123824]">{{ formatAmount(balance) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="sourceLinks.length" class="mt-6 space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="link in sourceLinks"
|
v-for="link in sourceLinks"
|
||||||
:key="link.id"
|
:key="link.id"
|
||||||
class="rounded-2xl bg-white px-3 py-2"
|
class="rounded-[24px] bg-[#f6fbf8] px-4 py-4 text-sm text-[#355947]"
|
||||||
>
|
>
|
||||||
<p class="font-medium text-[#123824]">{{ link.refereeName }}</p>
|
<p class="font-semibold text-[#123824]">{{ link.refereeName }}</p>
|
||||||
<p>{{ link.refereeCompanyName || link.refereeEmail }}</p>
|
<p class="mt-1">{{ link.refereeCompanyName || link.refereeEmail }}</p>
|
||||||
<p class="text-xs text-[#5c7b69]">Бонус: {{ link.bonusPercent }}%</p>
|
<p class="mt-2 text-xs text-[#5c7b69]">Бонус {{ link.bonusPercent }}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="rounded-2xl bg-white px-3 py-2 text-[#5c7b69]">
|
</template>
|
||||||
Активных связок пока нет.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -61,15 +61,7 @@ function formatDateTime(value: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="manager-hero">
|
<div class="space-y-4">
|
||||||
<p class="manager-eyebrow">Бонусы</p>
|
|
||||||
<h1 class="manager-title">{{ bonusAccount.fullName }}</h1>
|
|
||||||
<p class="manager-copy">
|
|
||||||
История начислений, активные связки и заявки на вывод по бонусной программе клиента.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-4 xl:grid-cols-[minmax(0,420px)_minmax(0,1fr)]">
|
|
||||||
<BonusAccountCard
|
<BonusAccountCard
|
||||||
:full-name="bonusAccount.fullName"
|
:full-name="bonusAccount.fullName"
|
||||||
:email="bonusAccount.email"
|
:email="bonusAccount.email"
|
||||||
@@ -79,70 +71,62 @@ function formatDateTime(value: string) {
|
|||||||
:source-links="bonusAccount.referralLinks"
|
:source-links="bonusAccount.referralLinks"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="surface-card rounded-[32px] p-6">
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Начисления</p>
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Заявки на вывод</h2>
|
|
||||||
<span class="text-sm text-[#5c7b69]">{{ pendingWithdrawals.length }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="pendingWithdrawals.length === 0" class="manager-empty-state mt-4">
|
<div v-if="transactions.length === 0" class="manager-empty-state mt-4">
|
||||||
Активных заявок на вывод нет.
|
Начислений пока нет.
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="mt-4 space-y-3">
|
|
||||||
<article
|
|
||||||
v-for="withdrawal in pendingWithdrawals"
|
|
||||||
:key="withdrawal.id"
|
|
||||||
class="rounded-3xl border border-[#deebe4] bg-[#f8fbf9] px-4 py-4"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-semibold text-[#123824]">{{ formatAmount(withdrawal.amount) }}</p>
|
|
||||||
<p class="text-sm text-[#355947]">Создано {{ formatDateTime(withdrawal.createdAt) }}</p>
|
|
||||||
<p v-if="withdrawal.reviewComment" class="text-sm text-[#355947]">{{ withdrawal.reviewComment }}</p>
|
|
||||||
</div>
|
|
||||||
<NuxtLink :to="`/bonus-system/withdrawals/${withdrawal.id}`" class="btn btn-accent btn-sm w-fit border-0">
|
|
||||||
Проверить выплату
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<div v-else class="mt-4 space-y-3">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<article
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Транзакции</h2>
|
v-for="transaction in transactions"
|
||||||
<span class="text-sm text-[#5c7b69]">{{ transactions.length }}</span>
|
:key="transaction.id"
|
||||||
</div>
|
class="rounded-[24px] bg-[#f6fbf8] px-4 py-4"
|
||||||
|
>
|
||||||
<div v-if="transactions.length === 0" class="manager-empty-state mt-4">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
Начислений по этой бонусной программе пока нет.
|
<div class="space-y-1">
|
||||||
</div>
|
<p class="text-base font-semibold text-[#123824]">+{{ formatAmount(transaction.amount) }}</p>
|
||||||
|
<p class="text-sm text-[#355947]">{{ transaction.reason }}</p>
|
||||||
<div v-else class="mt-4 space-y-3">
|
<p class="text-xs text-[#5c7b69]">{{ formatDateTime(transaction.createdAt) }}</p>
|
||||||
<article
|
|
||||||
v-for="transaction in transactions"
|
|
||||||
:key="transaction.id"
|
|
||||||
class="rounded-3xl border border-[#deebe4] bg-[#f8fbf9] px-4 py-4"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-base font-semibold text-[#123824]">+{{ formatAmount(transaction.amount) }}</p>
|
|
||||||
<p class="text-sm text-[#355947]">{{ transaction.reason }}</p>
|
|
||||||
<p class="text-xs text-[#5c7b69]">{{ formatDateTime(transaction.createdAt) }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
v-if="transaction.orderId"
|
|
||||||
:to="`/client-orders/${transaction.orderId}`"
|
|
||||||
class="btn btn-ghost btn-sm w-fit text-[#0d854a]"
|
|
||||||
>
|
|
||||||
Открыть заказ
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
</div>
|
<NuxtLink
|
||||||
|
v-if="transaction.orderId"
|
||||||
|
:to="`/client-orders/${transaction.orderId}`"
|
||||||
|
class="text-sm font-semibold text-[#0d854a]"
|
||||||
|
>
|
||||||
|
Открыть заказ
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="pendingWithdrawals.length" class="surface-card rounded-[32px] p-6">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Заявки на вывод</p>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
<article
|
||||||
|
v-for="withdrawal in pendingWithdrawals"
|
||||||
|
:key="withdrawal.id"
|
||||||
|
class="rounded-[24px] bg-[#f6fbf8] px-4 py-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-sm font-semibold text-[#123824]">{{ formatAmount(withdrawal.amount) }}</p>
|
||||||
|
<p class="text-sm text-[#355947]">Создано {{ formatDateTime(withdrawal.createdAt) }}</p>
|
||||||
|
<p v-if="withdrawal.reviewComment" class="text-sm text-[#355947]">{{ withdrawal.reviewComment }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/bonus-system/withdrawals/${withdrawal.id}`"
|
||||||
|
class="text-sm font-semibold text-[#0d854a]"
|
||||||
|
>
|
||||||
|
Проверить выплату
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { useQuery } from '@vue/apollo-composable';
|
|||||||
import {
|
import {
|
||||||
ManagerBonusBalancesDocument,
|
ManagerBonusBalancesDocument,
|
||||||
ManagerReferralLinksDocument,
|
ManagerReferralLinksDocument,
|
||||||
|
ManagerUsersDocument,
|
||||||
ManagerWithdrawalRequestsDocument,
|
ManagerWithdrawalRequestsDocument,
|
||||||
type ManagerBonusBalancesQuery,
|
type ManagerBonusBalancesQuery,
|
||||||
type ManagerReferralLinksQuery,
|
type ManagerReferralLinksQuery,
|
||||||
|
type ManagerUsersQuery,
|
||||||
type ManagerWithdrawalRequestsQuery,
|
type ManagerWithdrawalRequestsQuery,
|
||||||
} from '~/composables/graphql/generated';
|
} from '~/composables/graphql/generated';
|
||||||
|
import { messengerConnectionAvatarSrc } from '~/composables/useMessengerConnectionPresentation';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['manager-only'],
|
middleware: ['manager-only'],
|
||||||
@@ -15,6 +18,7 @@ definePageMeta({
|
|||||||
|
|
||||||
type BalanceItem = ManagerBonusBalancesQuery['managerBonusBalances'][number];
|
type BalanceItem = ManagerBonusBalancesQuery['managerBonusBalances'][number];
|
||||||
type ReferralLinkItem = ManagerReferralLinksQuery['managerReferralLinks'][number];
|
type ReferralLinkItem = ManagerReferralLinksQuery['managerReferralLinks'][number];
|
||||||
|
type ManagerUserItem = ManagerUsersQuery['managerUsers'][number];
|
||||||
type WithdrawalItem = ManagerWithdrawalRequestsQuery['managerWithdrawalRequests'][number];
|
type WithdrawalItem = ManagerWithdrawalRequestsQuery['managerWithdrawalRequests'][number];
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -22,6 +26,7 @@ const router = useRouter();
|
|||||||
const search = ref('');
|
const search = ref('');
|
||||||
const balancesQuery = useQuery(ManagerBonusBalancesDocument);
|
const balancesQuery = useQuery(ManagerBonusBalancesDocument);
|
||||||
const referralLinksQuery = useQuery(ManagerReferralLinksDocument);
|
const referralLinksQuery = useQuery(ManagerReferralLinksDocument);
|
||||||
|
const usersQuery = useQuery(ManagerUsersDocument);
|
||||||
const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, {
|
const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, {
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
});
|
});
|
||||||
@@ -41,8 +46,11 @@ function setTab(tab: 'balances' | 'withdrawals') {
|
|||||||
|
|
||||||
const balances = computed<BalanceItem[]>(() => balancesQuery.result.value?.managerBonusBalances ?? []);
|
const balances = computed<BalanceItem[]>(() => balancesQuery.result.value?.managerBonusBalances ?? []);
|
||||||
const referralLinks = computed<ReferralLinkItem[]>(() => referralLinksQuery.result.value?.managerReferralLinks ?? []);
|
const referralLinks = computed<ReferralLinkItem[]>(() => referralLinksQuery.result.value?.managerReferralLinks ?? []);
|
||||||
|
const users = computed<ManagerUserItem[]>(() => usersQuery.result.value?.managerUsers ?? []);
|
||||||
const withdrawals = computed<WithdrawalItem[]>(() => withdrawalsQuery.result.value?.managerWithdrawalRequests ?? []);
|
const withdrawals = computed<WithdrawalItem[]>(() => withdrawalsQuery.result.value?.managerWithdrawalRequests ?? []);
|
||||||
|
|
||||||
|
const usersById = computed(() => new Map(users.value.map((user) => [user.id, user])));
|
||||||
|
|
||||||
const referralLinksByReferrer = computed(() => {
|
const referralLinksByReferrer = computed(() => {
|
||||||
const grouped = new Map<string, ReferralLinkItem[]>();
|
const grouped = new Map<string, ReferralLinkItem[]>();
|
||||||
|
|
||||||
@@ -109,11 +117,18 @@ const filteredWithdrawals = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatAmount(value: number) {
|
function userInitials(fullName: string) {
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
const parts = fullName
|
||||||
minimumFractionDigits: 0,
|
.trim()
|
||||||
maximumFractionDigits: 2,
|
.split(/\s+/)
|
||||||
}).format(value);
|
.filter(Boolean)
|
||||||
|
.slice(0, 2);
|
||||||
|
|
||||||
|
if (!parts.length) {
|
||||||
|
return 'FR';
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -151,29 +166,27 @@ function formatAmount(value: number) {
|
|||||||
</UiSectionSearchHero>
|
</UiSectionSearchHero>
|
||||||
|
|
||||||
<template v-if="activeTab === 'balances'">
|
<template v-if="activeTab === 'balances'">
|
||||||
<div v-if="balancesQuery.loading.value || referralLinksQuery.loading.value" class="manager-empty-state">
|
<div v-if="balancesQuery.loading.value || referralLinksQuery.loading.value || usersQuery.loading.value" class="manager-empty-state">
|
||||||
Загружаем балансы...
|
Загружаем балансы...
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="filteredBalances.length === 0" class="manager-empty-state">
|
<div v-else-if="filteredBalances.length === 0" class="manager-empty-state">
|
||||||
Бонусных связок пока нет.
|
Бонусных связок пока нет.
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
<div v-else class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-6">
|
||||||
<BonusAccountCard
|
<NuxtLink
|
||||||
v-for="item in filteredBalances"
|
v-for="item in filteredBalances"
|
||||||
:key="item.userId"
|
:key="item.userId"
|
||||||
:full-name="item.fullName"
|
:to="`/bonus-system/${item.userId}`"
|
||||||
:email="item.email"
|
class="block surface-card-interactive"
|
||||||
:company-name="item.companyName"
|
>
|
||||||
:balance="item.balance"
|
<BonusAccountCard
|
||||||
:stats="[
|
:full-name="item.fullName"
|
||||||
{ label: 'Транзакций', value: String(item.transactionsCount) },
|
:balance="item.balance"
|
||||||
{ label: 'Активных связок', value: String(referralLinksByReferrer.get(item.userId)?.length ?? 0) },
|
:avatar-src="messengerConnectionAvatarSrc(usersById.get(item.userId)?.telegramConnection)"
|
||||||
{ label: 'На выводе', value: formatAmount(item.pendingWithdrawalAmount) },
|
:initials="userInitials(item.fullName)"
|
||||||
]"
|
compact
|
||||||
:source-links="referralLinksByReferrer.get(item.userId) ?? []"
|
/>
|
||||||
:detail-to="`/bonus-system/${item.userId}`"
|
</NuxtLink>
|
||||||
detail-label="Открыть"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user