Refine manager bonus and order detail views

This commit is contained in:
Ruslan Bakiev
2026-04-07 10:10:03 +07:00
parent 403aeea838
commit c70328d352
4 changed files with 144 additions and 69 deletions

View File

@@ -35,6 +35,26 @@ function formatAmount(value: number) {
function formatDateTime(value: string) {
return new Date(value).toLocaleString('ru-RU');
}
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]';
}
</script>
<template>
@@ -63,53 +83,73 @@ function formatDateTime(value: string) {
</template>
</UiBackHeader>
<div v-if="pendingWithdrawals.length" class="surface-card rounded-[32px] p-6">
<p class="text-lg font-bold text-[#123824]">Выводы</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 v-if="pendingWithdrawals.length" class="space-y-3">
<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>
<p class="text-lg font-bold text-[#123824]">Заявки на выплату</p>
<p class="text-sm text-[#5c7b69]">Все активные выплаты по этому бонусному счёту.</p>
</div>
<NuxtLink
v-for="withdrawal in pendingWithdrawals"
:key="withdrawal.id"
:to="`/admin/bonuses/requests/${withdrawal.id}`"
class="text-sm font-semibold text-[#0d854a]"
class="surface-card surface-card-interactive block rounded-[28px] px-5 py-4"
>
Проверить выплату
</NuxtLink>
</div>
</article>
</div>
<div class="grid gap-4 md:grid-cols-[180px_minmax(0,1fr)_170px_140px] md:items-center md:gap-6">
<div class="space-y-1">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-[#6a8a76]">Заявка</p>
<p class="text-base font-bold text-[#123824]">WD-{{ withdrawal.id.slice(-6).toUpperCase() }}</p>
</div>
<div class="surface-card rounded-[32px] p-6">
<div class="min-w-0 space-y-1">
<p class="text-sm font-semibold text-[#123824]">Создано {{ formatDateTime(withdrawal.createdAt) }}</p>
<p v-if="withdrawal.reviewComment" class="truncate text-sm text-[#5c7b69]">{{ withdrawal.reviewComment }}</p>
</div>
<div>
<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>
</div>
<div class="space-y-3">
<div class="space-y-1">
<p class="text-lg font-bold text-[#123824]">Транзакции</p>
<p class="text-sm text-[#5c7b69]">История начислений и операций по бонусному счёту.</p>
</div>
<div v-if="transactions.length === 0" class="manager-empty-state mt-4">
Начислений пока нет.
</div>
<div v-else class="mt-4 space-y-3">
<div v-else class="space-y-3">
<article
v-for="transaction in transactions"
:key="transaction.id"
class="rounded-[24px] bg-[#f6fbf8] px-4 py-4"
class="surface-card rounded-[28px] px-5 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 class="grid gap-4 md:grid-cols-[140px_minmax(0,1fr)_180px_140px] md:items-center md:gap-6">
<div>
<p class="text-base font-bold text-[#123824]">+{{ formatAmount(transaction.amount) }} </p>
</div>
<div class="min-w-0 space-y-1">
<p class="text-sm font-semibold text-[#123824]">Начисление</p>
<p class="text-sm text-[#355947]">{{ transaction.reason }}</p>
</div>
<div class="text-sm text-[#5c7b69]">
{{ formatDateTime(transaction.createdAt) }}
</div>
<div class="text-left md:text-right">
<NuxtLink
v-if="transaction.orderId"
:to="`/admin/orders/${transaction.orderId}`"
@@ -118,6 +158,7 @@ function formatDateTime(value: string) {
Открыть заказ
</NuxtLink>
</div>
</div>
</article>
</div>
</div>

View File

@@ -292,6 +292,10 @@ function productVisualLabel(product: ProductCard) {
.map((part) => part.charAt(0).toUpperCase())
.join('');
}
function compactProductTitle(product: ProductCard) {
return `Подарочная карта ${product.store}`;
}
</script>
<template>
@@ -356,14 +360,15 @@ function productVisualLabel(product: ProductCard) {
{{ 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]">
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full bg-[#eef5f0] px-3 py-1 text-xs font-bold uppercase tracking-[0.12em] text-[#4e7060]">
{{ product.store }}
</span>
<span class="rounded-full bg-[#fff8dc] px-3 py-1 text-xs font-bold text-[#7a5b00]">
{{ formatAmount(product.amount) }}
</span>
</div>
<h2 class="mt-3 text-lg font-bold leading-tight text-[#123824]">{{ compactProductTitle(product) }}</h2>
</div>
</div>
</article>

View File

@@ -21,14 +21,22 @@ const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, {
});
const reviewMutation = useMutation(ReviewRewardWithdrawalDocument);
const decision = ref<'APPROVE' | 'REJECT'>('APPROVE');
const reviewComment = ref('');
const reviewResult = ref('');
const isProcessed = ref(true);
const savePending = computed(() => reviewMutation.loading.value);
const currentWithdrawal = computed(() =>
(withdrawalsQuery.result.value?.managerWithdrawalRequests ?? []).find((item: WithdrawalItem) => item.id === withdrawalId.value),
);
watch(currentWithdrawal, (withdrawal) => {
if (!withdrawal) {
return;
}
isProcessed.value = withdrawal.status !== 'REJECTED';
}, { immediate: true });
async function reviewWithdrawal() {
if (!currentWithdrawal.value) {
return;
@@ -37,8 +45,7 @@ async function reviewWithdrawal() {
const response = await reviewMutation.mutate({
input: {
withdrawalId: currentWithdrawal.value.id,
decision: decision.value,
reviewComment: reviewComment.value || undefined,
decision: isProcessed.value ? 'APPROVE' : 'REJECT',
},
});
@@ -48,7 +55,7 @@ async function reviewWithdrawal() {
</script>
<template>
<section class="space-y-6 max-w-3xl">
<section class="space-y-6">
<div v-if="withdrawalsQuery.loading.value" class="manager-empty-state">
Загружаем заявку на вывод...
</div>
@@ -65,22 +72,35 @@ async function reviewWithdrawal() {
:subtitle="`${currentWithdrawal.requesterFullName} · ${currentWithdrawal.requesterEmail} · Сумма: ${currentWithdrawal.amount}`"
/>
<div class="surface-card rounded-3xl p-5 space-y-3">
<label class="form-control">
<span class="label-text">Решение</span>
<select v-model="decision" class="select manager-field w-full">
<option value="APPROVE">Одобрить</option>
<option value="REJECT">Отклонить</option>
</select>
<div class="surface-card rounded-3xl p-5 md:p-6">
<div class="space-y-5">
<label class="flex items-start gap-4 rounded-[24px] bg-[#f5faf7] px-4 py-4">
<input
v-model="isProcessed"
type="checkbox"
class="checkbox mt-1 border-[#b9d7c5] bg-white [--chkbg:#123824] [--chkfg:#ffffff]"
>
<span class="space-y-1">
<span class="block text-base font-bold text-[#123824]">Проведено</span>
<span class="block text-sm leading-6 text-[#5c7b69]">
Отметьте выплату как проведённую. Если галочка снята, заявка будет отклонена.
</span>
</span>
</label>
<label class="form-control">
<span class="label-text">Комментарий</span>
<textarea v-model="reviewComment" class="textarea manager-field min-h-28 w-full" placeholder="Комментарий для заявки" />
</label>
<div>
<button class="btn btn-primary border-0" @click="reviewWithdrawal">Сохранить решение</button>
<button
class="btn h-12 w-full rounded-full border-0 bg-[#123824] text-white shadow-[0_16px_32px_rgba(18,56,36,0.18)] hover:bg-[#0f2f20] disabled:border-0 disabled:bg-[#cfd8d2] disabled:text-[#6f8579]"
:disabled="savePending"
@click="reviewWithdrawal"
>
{{
savePending
? 'Сохраняем...'
: isProcessed
? 'Провести выплату'
: 'Отклонить заявку'
}}
</button>
</div>
</div>

View File

@@ -320,7 +320,16 @@ watch(
to="/admin/orders"
back-label="Назад к заказам клиентов"
:title="`Заказ ${currentOrderCode}`"
/>
>
<template #actions>
<NuxtLink
:to="`/admin/orders/clients/${currentOrder.customerId}`"
class="btn rounded-full border border-[#d7e6dc] bg-white px-5 text-[#123824] hover:border-[#bed6c7] hover:bg-[#f6fbf8]"
>
Открыть клиента
</NuxtLink>
</template>
</UiBackHeader>
<div class="space-y-4">
<div class="surface-card rounded-3xl p-5">