@@ -25,12 +25,11 @@ type ProductCard = {
store : string ;
title : string ;
amount : number ;
subtitle : string ;
gradient : string ;
tags : string [ ] ;
} ;
const route = useRoute ( ) ;
const router = useRouter ( ) ;
const search = ref ( '' ) ;
const balancesQuery = useQuery ( ManagerBonusBalancesDocument ) ;
const referralLinksQuery = useQuery ( ManagerReferralLinksDocument ) ;
@@ -39,12 +38,12 @@ const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, {
status : 'PENDING' ,
} ) ;
const activeTab = computed < 'balances' | 'withdrawals' | 'product s' > ( ( ) => {
const activeTab = computed < 'balances' | 'withdrawals' | 'reward s' > ( ( ) => {
if ( route . query . tab === 'withdrawals' ) {
return 'withdrawals' ;
}
if ( route . query . tab === 'products' || route . query . tab === 'manager' ) {
return 'product s' ;
if ( route . query . tab === 'rewards' || route . query . tab === 'products' || route . query . tab === 'manager' ) {
return 'reward s' ;
}
return 'balances' ;
} ) ;
@@ -55,54 +54,42 @@ const productCards: ProductCard[] = [
store : 'Ozon' ,
title : 'Подарочная карта Ozon' ,
amount : 3000 ,
subtitle : 'Универсальная карта для маркетплейса: техника, дом и повседневные покупки.' ,
gradient : 'linear-gradient(135deg, #38b6ff 0%, #1369ff 55%, #0b2f72 100%)' ,
tags : [ 'Маркетплейс' , 'Электронная карта' , '3 000 ₽' ] ,
} ,
{
id : 'ozon-5000' ,
store : 'Ozon' ,
title : 'Подарочная карта Ozon' ,
amount : 5000 ,
subtitle : 'Крупный номинал для заметных подарков и сезонных закупок.' ,
gradient : 'linear-gradient(135deg, #65d0ff 0%, #247bff 52%, #12315e 100%)' ,
tags : [ 'Маркетплейс' , 'Топ-номинал' , '5 000 ₽' ] ,
} ,
{
id : 'wildberries-3000' ,
store : 'Wildberries' ,
title : 'Подарочная карта Wildberries' ,
amount : 3000 ,
subtitle : 'Подходит для одежды, дома и повседневных мелочей в одном каталоге.' ,
gradient : 'linear-gradient(135deg, #d84dff 0%, #8b27ff 52%, #39006a 100%)' ,
tags : [ 'Fashion' , 'Маркетплейс' , '3 000 ₽' ] ,
} ,
{
id : 'wildberries-4000' ,
store : 'Wildberries' ,
title : 'Подарочная карта Wildberries' ,
amount : 4000 ,
subtitle : 'Средний номинал для fashion-покупок и товаров для дома.' ,
gradient : 'linear-gradient(135deg, #ef7cff 0%, #a12dff 50%, #4c0b7d 100%)' ,
tags : [ 'Одежда' , 'Дом' , '4 000 ₽' ] ,
} ,
{
id : 'mvideo-4000' ,
store : 'М .Видео' ,
title : 'Подарочная карта М .Видео' ,
amount : 4000 ,
subtitle : 'Для техники, аксессуаров и бытовой электроники.' ,
gradient : 'linear-gradient(135deg, #ff9461 0%, #ff5630 48%, #821414 100%)' ,
tags : [ 'Техника' , 'Электроника' , '4 000 ₽' ] ,
} ,
{
id : 'mvideo-5000' ,
store : 'М .Видео' ,
title : 'Подарочная карта М .Видео' ,
amount : 5000 ,
subtitle : 'Максимальный номинал для заметных подарков и апгрейдов рабочего места.' ,
gradient : 'linear-gradient(135deg, #ffb17e 0%, #ff6842 50%, #8f1818 100%)' ,
tags : [ 'Электроника' , 'Подарок' , '5 000 ₽' ] ,
} ,
] ;
@@ -196,9 +183,7 @@ const filteredProducts = computed(() => {
return [
item . store ,
item . title ,
item . subtitle ,
String ( item . amount ) ,
... item . tags ,
]
. join ( ' ' )
. toLowerCase ( )
@@ -230,6 +215,31 @@ const {
resetKeys : [ search , activeTab ] ,
} ) ;
const WITHDRAWAL _DATE _FORMATTER = new Intl . DateTimeFormat ( 'ru-RU' , {
day : '2-digit' ,
month : 'short' ,
hour : '2-digit' ,
minute : '2-digit' ,
} ) ;
const bonusTabs = computed < Array < { id : ' balances ' | ' withdrawals ' | ' rewards ' ; label : string } > > ( ( ) => [
{ id : 'balances' , label : 'Балансы' } ,
{ id : 'withdrawals' , label : 'Выплаты' } ,
{ id : 'rewards' , label : 'Вознаграждения' } ,
] ) ;
function setActiveTab ( tab : 'balances' | 'withdrawals' | 'rewards' ) {
const query = { ... route . query } ;
if ( tab === 'balances' ) {
delete query . tab ;
} else {
query . tab = tab ;
}
void router . replace ( { query } ) ;
}
function userInitials ( fullName : string ) {
const parts = fullName
. trim ( )
@@ -250,6 +260,55 @@ function formatAmount(value: number) {
maximumFractionDigits : 2 ,
} ) . format ( value ) ;
}
function formatWithdrawalCode ( id : string ) {
return ` WD- ${ id . slice ( - 6 ) . toUpperCase ( ) } ` ;
}
function formatDateTime ( value : string ) {
return WITHDRAWAL _DATE _FORMATTER . format ( new Date ( value ) ) ;
}
function withdrawalStatusLabel ( status : string ) {
if ( status === 'APPROVED' ) {
return 'Подтверждена' ;
}
if ( status === 'REJECTED' ) {
return 'Отклонена' ;
}
return 'Н а проверке' ;
}
function withdrawalStatusClass ( status : string ) {
if ( status === 'APPROVED' ) {
return 'bg-[#def7e8] text-[#0d854a]' ;
}
if ( status === 'REJECTED' ) {
return 'bg-[#fde8ea] text-[#b73742]' ;
}
return 'bg-[#fff3d8] text-[#9a6100]' ;
}
function requesterMeta ( withdrawal : WithdrawalItem ) {
const requester = usersById . value . get ( withdrawal . requesterId ) ;
const fullName = withdrawal . requesterFullName ;
return {
avatarSrc : messengerConnectionAvatarSrc ( requester ? . telegramConnection ) ,
initials : userInitials ( fullName ) ,
companyName : withdrawal . companyName || requester ? . companyName || '' ,
} ;
}
function productVisualLabel ( product : ProductCard ) {
return product . store
. replace ( /[^A-Za-zА -Яа-яЁё0-9]+/g , ' ' )
. trim ( )
. split ( /\s+/ )
. slice ( 0 , 2 )
. map ( ( part ) => part . charAt ( 0 ) . toUpperCase ( ) )
. join ( '' ) ;
}
< / script >
< template >
@@ -258,12 +317,29 @@ function formatAmount(value: number) {
v-model = "search"
title = "Бонусы"
: search -placeholder = " activeTab = = = ' balances '
? ' Клиент , связанный клиент , email или процент '
? ' Клиент , связанный клиент или email '
: activeTab = = = ' withdrawals '
? ' Пользователь , сумма или с татус '
: ' Магазин , номинал или тип карты ' "
? ' Номер выплаты , клиент или с умма '
: ' Название или номинал ' "
/ >
< div class = "surface-card rounded-3xl p-2" >
< div class = "flex flex-wrap gap-2" >
< button
v-for = "tab in bonusTabs"
:key = "tab.id"
type = "button"
class = "rounded-full px-4 py-2 text-sm font-semibold transition"
: class = "activeTab === tab.id
? 'bg-[#123824] text-white'
: 'bg-[#eef7f1] text-[#355947] hover:bg-[#e3f1e8]'"
@click ="setActiveTab(tab.id)"
>
{ { tab . label } }
< / button >
< / div >
< / div >
< template v-if = "activeTab === 'balances'" >
< div v-if = "balancesQuery.loading.value || referralLinksQuery.loading.value || usersQuery.loading.value" class="manager-empty-state" >
Загружаем балансы...
@@ -280,8 +356,7 @@ function formatAmount(value: number) {
:full-name = "item.fullName"
:avatar-src = "messengerConnectionAvatarSrc(usersById.get(item.userId)?.telegramConnection)"
:initials = "userInitials(item.fullName)"
meta -label = " Доступный бонус "
:meta-value = "formatAmount(item.balance)"
: meta -value = " ` $ { formatAmount ( item.balance ) } ₽ ` "
/ >
< / div >
@@ -297,25 +372,7 @@ function formatAmount(value: number) {
< / div >
< / template >
< template v-else-if = "activeTab === 'product s'" >
< div class = "surface-card rounded-[32px] p-6" >
< div class = "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between" >
< div class = "space-y-2" >
< p class = "text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]" > Витрина магазина < / p >
< h2 class = "text-2xl font-black tracking-[-0.03em] text-[#123824]" > Подарочные карты для бонусного каталога < / h2 >
< p class = "max-w-3xl text-sm leading-6 text-[#557562]" >
Вынес товары в отдельную вкладку без лишнего вложения . Пока это стартовый сет из популярных магазинов с номиналами от 3 000 до 5 000 рублей .
< / p >
< / div >
< div class = "flex flex-wrap gap-2 text-sm text-[#355947]" >
< span class = "rounded-full bg-[#eef7f1] px-3 py-2 font-semibold" > 3 магазина < / span >
< span class = "rounded-full bg-[#eef7f1] px-3 py-2 font-semibold" > 6 карточек < / span >
< span class = "rounded-full bg-[#eef7f1] px-3 py-2 font-semibold" > 3 000 - 5 000 ₽ < / span >
< / div >
< / div >
< / div >
< template v-else-if = "activeTab === 'reward s'" >
< div v-if = "filteredProducts.length === 0" class="manager-empty-state" >
По текущему запросу товары не найдены.
< / div >
@@ -323,36 +380,24 @@ function formatAmount(value: number) {
< article
v-for = "product in filteredProducts"
:key = "product.id"
class = "surface-card overflow-hidden rounded-[3 2px]"
class = "surface-card rounded-[28 px] p-5 "
>
< div class = "p-5 text-white" : style = "{ background: product.gradient } " >
< div class = "flex items-start justify-between gap-4" >
< div >
< p class = "text-[11px] font-semibold uppercase tracking-[0.18em] text-white/72" > { { product . store } } < / p >
< h3 class = "mt-3 text-2xl font-black tracking-[-0.03em]" > { { product . title } } < / h3 >
< / div >
< span class = "rounded-full bg-white/14 px-3 py-1 text-sm font-semibold backdrop-blur-sm" > Витрина < / span >
< / div >
< div class = "mt-10" >
< p class = "text-sm text-white/72" > Номинал < / p >
< p class = "mt-2 text-4xl font-black leading-none" > { { formatAmount ( product . amount ) } } ₽ < / p >
< / div >
< / div >
< div class = "space-y-4 p-5" >
< p class = "text-sm leading-6 text-[#557562]" >
{ { product . subtitle } }
< / p >
< div class = "flex flex-wrap gap-2" >
< span
v-for = "tag in product.tags"
:key = "tag"
class = "rounded-full bg-[#eef7f1] px-3 py-1.5 text-xs font-semibold text-[#0d854a]"
< div class = "flex items-center gap-4 " >
< div
class = "flex h-16 w-16 shrink-0 items-center justify-center rounded-[20px] text-lg font-black text-white"
: style = "{ background: product.gradient }"
>
{ { tag } }
< / span >
{ { productVisualLabel ( product ) } }
< / div >
< div class = "min-w-0 flex-1" >
< p class = "text-sm font-semibold text-[#6a8a76]" > { { product . store } } < / p >
< h2 class = "mt-1 text-lg font-bold leading-tight text-[#123824]" > { { product . title } } < / h2 >
< / div >
< / div >
< div class = "mt-5 flex items-center justify-between gap-3" >
< p class = "text-sm text-[#557562]" > Номинал < / p >
< div class = "rounded-full bg-[#fff8dc] px-4 py-2 text-sm font-bold text-[#123824]" >
{ { formatAmount ( product . amount ) } } ₽
< / div >
< / div >
< / article >
@@ -366,25 +411,55 @@ function formatAmount(value: number) {
< div v-else-if = "filteredWithdrawals.length === 0" class="manager-empty-state" >
Активных заявок на выплату сейчас нет.
< / div >
< div v-else class = "space-y-4 " >
< article
< div v-else class = "space-y-3 " >
< NuxtLink
v-for = "withdrawal in visibleWithdrawals"
:key = "withdrawal.id"
class = "surface-card rounded-3xl px-5 py-5 "
:to = "`/bonus-system/withdrawals/${withdrawal.id}` "
class = "surface-card surface-card-interactive block rounded-[30px] bg-white px-4 py-4 md:px-5"
>
< div class = "flex flex-col gap-3 md:flex-row md:items-start md:justify-between " >
< div class = "space-y-1 " >
< p class = "text-sm font-semi bold text-[#123824]" > { { w ithdrawal. requesterFullName } } < / p >
< p class = "text-sm text-[#355947 ]" > { { withdrawal . requesterEmail } } < / p >
< p v-if = "withdrawal.companyName" class="text-sm text-[#355947]" > {{ withdrawal.companyName }} < / p >
< p class = "text-sm text-[#355947]" > Сумма : { { withdrawal . amount } } < / p >
< p class = "text-xs text-[#5c7b69]" > { { new Date ( withdrawal . createdAt ) . toLocaleString ( ) } } < / p >
< div class = "grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)_180px_140px] md:items-center md:gap-6 " >
< div class = "min-w-0 " >
< h2 class = "truncate text-lg font-bold text-[#123824]" > { { formatW ithdrawalCode ( withdrawal . id ) } } < / h2 >
< p class = "mt-1 text-sm text-[#688676 ]" > { { formatDateTime ( withdrawal. c reatedAt ) } } < / p >
< / div >
< div class = "flex min-w-0 items-center gap-3" >
< img
v-if = "requesterMeta(withdrawal).avatarSrc"
:src = "requesterMeta(withdrawal).avatarSrc"
:alt = "withdrawal.requesterFullName"
class = "h-12 w-12 rounded-[16px] object-cover"
>
< div
v-else
class = "flex h-12 w-12 shrink-0 items-center justify-center rounded-[16px] bg-[linear-gradient(135deg,#dff7e9_0%,#c2ead3_100%)] text-sm font-black text-[#123824]"
>
{ { requesterMeta ( withdrawal ) . initials } }
< / div >
< div class = "min-w-0" >
< p class = "truncate text-sm font-semibold text-[#123824]" > { { withdrawal . requesterFullName } } < / p >
< p class = "truncate text-sm text-[#557562]" >
{ { requesterMeta ( withdrawal ) . companyName || withdrawal . requesterEmail } }
< / p >
< / div >
< / div >
< div class = "flex items-center justify-start" >
< span
class = "rounded-full px-3 py-1 text-sm font-semibold"
:class = "withdrawalStatusClass(withdrawal.status)"
>
{ { withdrawalStatusLabel ( withdrawal . status ) } }
< / span >
< / div >
< div class = "text-left md:text-right" >
< p class = "text-base font-bold text-[#123824]" > { { formatAmount ( withdrawal . amount ) } } ₽ < / p >
< / div >
< / div >
< NuxtLink :to = "`/bonus-system/withdrawals/${withdrawal.id}`" class = "btn btn-accent btn-sm border-0" >
Проверить выплату
< / NuxtLink >
< / div >
< / article >
< div
v-if = "canLoadMoreWithdrawals"