feat: add deposit receipt preview, deposit identifier, and approve/reject modals with SMS

This commit is contained in:
2026-11-08 13:27:23 +03:30
parent 379995e0ab
commit e0e2b6daa5
9 changed files with 334 additions and 10 deletions
+6
View File
@@ -28,6 +28,12 @@ export const exportWithdrawReport = async (params: TableInput & { flow_type?: st
export const completeWithdraw = async (params: Withdraw) =>
await useAxios.post<boolean>('/admin/wallet/withdraw/complete', params)
export const approveDeposit = async (params: { id: number; tracked_number?: string }) =>
await useAxios.post<{ message: string; withdraw: Withdraw }>('/admin/wallet/deposit/approve', params)
export const rejectDeposit = async (params: { id: number; reject_reason?: string }) =>
await useAxios.post<{ message: string; withdraw: Withdraw }>('/admin/wallet/deposit/reject', params)
export const updateSts = async (id: number, sts: Object) => {
return await useAxios.put(`/admin/wallet/withdraw/${id}`, sts)
}
+2 -1
View File
@@ -13,10 +13,11 @@ export interface User {
lname?: string
mobile?: number
name?: string
n_code?: string
sts?: number
token_sms?: string
user_type?: string
send_sms?:boolean
send_sms?: boolean
settings?: Const[]
sms_billing_enabled?: boolean
sms_panel_access?: boolean
+4
View File
@@ -84,6 +84,10 @@ export interface Withdraw {
date_at?: string
sts?: number
type?: number
receipt?: string | null
receipt_url?: string | null
deposit_identifier?: string | null
reject_reason?: string | null
bank_account_id?: number | null
created_at?: string
updated_at?: string
@@ -24,6 +24,8 @@
:item="selectedItem"
@withdraw="(row) => emit('withdraw', row)"
@weight="(row) => emit('weight', row)"
@approve-deposit="(row) => emit('approveDeposit', row)"
@reject-deposit="(row) => emit('rejectDeposit', row)"
@change-status="changeStatusHandler"
/>
</template>
@@ -53,6 +55,8 @@ const props = withDefaults(
const emit = defineEmits<{
(e: 'withdraw', item: Withdraw): void
(e: 'weight', item: Withdraw): void
(e: 'approveDeposit', item: Withdraw): void
(e: 'rejectDeposit', item: Withdraw): void
}>()
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Withdraw>()
@@ -6,8 +6,24 @@
<div class="flex w-full items-center justify-between gap-3">
<span class="min-w-0 truncate">{{ sheetTitle }}</span>
<div class="flex shrink-0 items-center gap-2">
<template v-if="isPendingDepositRequest(row)">
<MobileIconButton
plain
icon="vuesax-linear:tick-circle"
color="#16A34A"
:size="22"
@click="$emit('approveDeposit', row)"
/>
<MobileIconButton
plain
icon="vuesax-linear:close-circle"
color="#DC2626"
:size="22"
@click="$emit('rejectDeposit', row)"
/>
</template>
<MobileIconButton
v-if="needsTrackingConfirmation(row)"
v-else-if="needsTrackingConfirmation(row)"
plain
icon="vuesax-linear:tick-circle"
color="#16A34A"
@@ -103,8 +119,51 @@
<MobileDetailRow label="نام و نام خانوادگی" full-width :value="row.customer?.name || '-'" />
<MobileDetailRow label="شماره کارت" full-width :value="row.bank_account?.cart_number || '-'" />
<MobileDetailRow label="شماره شبا" full-width :value="row.bank_account?.shaba_number || '-'" />
<MobileDetailRow
label="شناسه واریز"
full-width
:value="row.deposit_identifier || row.customer?.n_code || '-'"
/>
<MobileDetailRow label="تاریخ ثبت" full-width :value="dateLabel(row)" />
<MobileDetailRow
v-if="row.reject_reason"
label="علت رد درخواست"
full-width
:value="row.reject_reason"
/>
<div v-if="row.receipt_url" class="col-span-2 py-2">
<div class="text-xs text-gray-500 mb-1">فیش واریزی:</div>
<a-image
:src="row.receipt_url"
:height="120"
class="rounded-lg object-contain border max-w-full"
alt="فیش واریزی"
/>
</div>
</MobileInfoGrid>
<div v-if="isPendingDepositRequest(row)" class="mt-4 flex gap-2">
<BaseButton
type="primary"
class="flex-1 !bg-emerald-600 hover:!bg-emerald-700 !border-emerald-600 text-white"
@click="$emit('approveDeposit', row)"
>
<template #icon>
<BaseIcon icon="vuesax-linear:tick-circle" size="16" />
</template>
تایید واریز
</BaseButton>
<BaseButton
danger
class="flex-1"
@click="$emit('rejectDeposit', row)"
>
<template #icon>
<BaseIcon icon="vuesax-linear:close-circle" size="16" />
</template>
رد واریز
</BaseButton>
</div>
</template>
</MobileDetailSheet>
</template>
@@ -121,6 +180,7 @@ import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
import {
formatWithdrawAmountDisplay,
isMoneyWithdraw,
isPendingDepositRequest,
needsTrackingConfirmation,
needsWeightAssignment,
withdrawAmountValue,
@@ -140,6 +200,8 @@ const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'withdraw', item: Withdraw): void
(e: 'weight', item: Withdraw): void
(e: 'approveDeposit', item: Withdraw): void
(e: 'rejectDeposit', item: Withdraw): void
(e: 'changeStatus', item: Withdraw, statusEntry: [string, (typeof status)[keyof typeof status]]): void
}>()
@@ -11,15 +11,35 @@
</template>
<template #tracked="{ column, record }">
<div v-if="record.tracked_number">{{ record.tracked_number }}</div>
<div v-else-if="needsTrackingConfirmation(record)">
<div v-else-if="!isPendingDepositRequest(record) && needsTrackingConfirmation(record)">
<BaseButton v-if="can('confirm')" @click="emits('withdraw', record)">
{{ confirmLabelForRecord(record, props.flowType) }}
</BaseButton>
</div>
<div v-else>-</div>
</template>
<template #receipt="{ column, record }">
<div v-if="record.receipt_url" class="flex justify-center items-center">
<a-image
:src="record.receipt_url"
:width="36"
:height="36"
class="rounded object-cover border border-gray-200 cursor-pointer shadow-sm hover:opacity-80 transition"
alt="فیش واریز"
/>
</div>
<span v-else class="text-gray-400">-</span>
</template>
<template #action="{ column, record }">
<div class="gap-2 flex justify-center">
<div class="gap-2 flex justify-center items-center">
<a-tooltip v-if="record.reject_reason" class="cursor-pointer">
<template #title>
<span class="whitespace-pre-wrap">علت رد: {{ record.reject_reason }}</span>
</template>
<a-badge dot color="#dc2626">
<Icon icon="vuesax-linear:info-circle" color="#dc2626" />
</a-badge>
</a-tooltip>
<a-tooltip v-if="record.extra_note" class="cursor-pointer">
<template #title>
<span class="whitespace-pre-wrap">{{ record.extra_note }}</span>
@@ -28,7 +48,34 @@
<Icon icon="vuesax-linear:message-text" color="#64748b" />
</a-badge>
</a-tooltip>
<a-tooltip v-if="can('confirm') && needsTrackingConfirmation(record)" class="cursor-pointer">
<template v-if="isPendingDepositRequest(record) && can('confirm')">
<a-tooltip title="تایید واریز و شارژ کیف پول">
<BaseButton
size="small"
type="primary"
class="!bg-emerald-600 hover:!bg-emerald-700 !border-emerald-600 text-white"
@click="emits('approveDeposit', record)"
>
<template #icon>
<Icon icon="vuesax-linear:tick-circle" size="14" />
</template>
تایید
</BaseButton>
</a-tooltip>
<a-tooltip title="رد واریز">
<BaseButton
size="small"
danger
@click="emits('rejectDeposit', record)"
>
<template #icon>
<Icon icon="vuesax-linear:close-circle" size="14" />
</template>
رد
</BaseButton>
</a-tooltip>
</template>
<a-tooltip v-else-if="can('confirm') && needsTrackingConfirmation(record)" class="cursor-pointer">
<template #title><span>{{ confirmLabelForRecord(record, props.flowType) }}</span></template>
<BaseButton size="small" type="primary" @click="emits('withdraw', record)">
{{ confirmLabelForRecord(record, props.flowType) }}
@@ -101,6 +148,8 @@
:flow-type="props.flowType"
@withdraw="(row) => emits('withdraw', row)"
@weight="(row) => emits('weight', row)"
@approve-deposit="(row) => emits('approveDeposit', row)"
@reject-deposit="(row) => emits('rejectDeposit', row)"
/>
</div>
@@ -138,6 +187,7 @@ import { isMobile } from '@core/utils/is'
import type { WithdrawFlowType } from '../types'
import {
confirmLabelForRecord,
isPendingDepositRequest,
needsTrackingConfirmation,
needsWeightAssignment
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
@@ -166,7 +216,7 @@ const zarinEnabled = zarinEnabledSetting
val.sts === 1 ? return ['1'] : ['2']
}*/
//emits
const emits = defineEmits(['delete', 'edit', 'add', 'withdraw', 'weight'])
const emits = defineEmits(['delete', 'edit', 'add', 'withdraw', 'weight', 'approveDeposit', 'rejectDeposit'])
const { prefixCls } = usePrefix('pophoverstatus')
@@ -74,6 +74,24 @@ export function getWithdrawColumns(): BaseTableColumn[] {
key: 'customer.name',
type: 'Text'
},
{
title: 'شناسه واریز',
dataIndex: 'deposit_identifier',
key: 'deposit_identifier',
type: 'Text',
customRender: ({ record }: any) => {
const val = record?.deposit_identifier || record?.customer?.n_code
return val ? <span dir="ltr" class="font-mono text-xs">{val}</span> : '-'
}
},
{
title: 'فیش واریزی',
dataIndex: 'receipt_url',
key: 'receipt_url',
slot: 'receipt',
filterable: false,
sortable: false
},
{
title: 'نوع',
dataIndex: 'asset_type',
@@ -172,9 +190,18 @@ export function getWithdrawColumns(): BaseTableColumn[] {
)
}
}*/
customRender: ({ record }) => {
customRender: ({ record }: any) => {
let textColor = '#ca8a04'
let bgColor = '#fef3c7'
if (record?.sts === 2 || record?.sts === 4) {
textColor = '#25855A'
bgColor = '#E3F4E9'
} else if (record?.sts === 3) {
textColor = '#C53030'
bgColor = '#FECACA'
}
return (
<CustomTag textColor='#ca8a04' bgColor='#fef3c7'>{record?.sts_str}</CustomTag>
<CustomTag textColor={textColor} bgColor={bgColor}>{record?.sts_str || '-'}</CustomTag>
)
},
},
+163 -2
View File
@@ -24,6 +24,8 @@
@delete="deleteItem"
@withdraw="openWithdraw"
@weight="openWeight"
@approve-deposit="openApproveDeposit"
@reject-deposit="openRejectDeposit"
/>
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" />
@@ -92,6 +94,95 @@
اطلاعات تراکنش یافت نشد
</div>
</BasicModal>
<!-- مدال تایید واریز وجه -->
<BasicModal
v-model:visible="approveDepositModalVisible"
width="28rem"
title="تایید واریز وجه به کیف پول"
wrap-class-name="withdraw-stack-modal"
:loading="depositActionLoading"
@ok="handleApproveDeposit"
@close="approveDepositModalVisible = false"
>
<div v-if="selectedDepositRecord" class="space-y-3 py-2 text-sm">
<div class="rounded-lg bg-emerald-50 p-3 text-emerald-900 border border-emerald-200">
<div class="font-bold flex justify-between items-center">
<span>مشتری: {{ selectedDepositRecord.customer?.name || '-' }}</span>
<span class="text-xs text-emerald-700">موبایل: {{ selectedDepositRecord.customer?.mobile || '-' }}</span>
</div>
<div class="mt-2 text-xs flex justify-between">
<span>شناسه واریز: <strong dir="ltr">{{ selectedDepositRecord.deposit_identifier || selectedDepositRecord.customer?.n_code || '-' }}</strong></span>
<span>مبلغ: <strong class="text-emerald-800 text-sm">{{ formatWithdrawAmountDisplay(selectedDepositRecord) }}</strong></span>
</div>
</div>
<div v-if="selectedDepositRecord.receipt_url" class="rounded border p-2 text-center bg-gray-50">
<div class="text-xs text-gray-500 mb-2">تصویر فیش واریزی (جهت بزرگنمایی روی عکس کلیک کنید):</div>
<a-image
:src="selectedDepositRecord.receipt_url"
:height="150"
class="rounded object-contain mx-auto max-w-full"
alt="فیش واریز"
/>
</div>
<div v-if="selectedDepositRecord.extra_note" class="rounded bg-gray-100 p-2 text-xs text-gray-700">
<span class="font-bold">توضیحات مشتری: </span>
<span>{{ selectedDepositRecord.extra_note }}</span>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">شماره پیگیری تراکنش:</label>
<BasicInput
v-model:value="approveTrackedNumber"
placeholder="شماره پیگیری فیش بانکی"
dir="ltr"
/>
</div>
<div class="text-xs text-gray-500">
با تایید، مبلغ به کیف پول نقدی مشتری واریز شده و پیامک تایید ارسال می‌گردد.
</div>
</div>
</BasicModal>
<!-- مدال رد واریز وجه -->
<BasicModal
v-model:visible="rejectDepositModalVisible"
width="26rem"
title="رد درخواست واریز"
wrap-class-name="withdraw-stack-modal"
:loading="depositActionLoading"
@ok="handleRejectDeposit"
@close="rejectDepositModalVisible = false"
>
<div v-if="selectedDepositRecord" class="space-y-3 py-2 text-sm">
<div class="rounded-lg bg-red-50 p-3 text-red-900 border border-red-200">
<div class="font-bold flex justify-between items-center">
<span>مشتری: {{ selectedDepositRecord.customer?.name || '-' }}</span>
<span class="text-xs text-red-700">موبایل: {{ selectedDepositRecord.customer?.mobile || '-' }}</span>
</div>
<div class="mt-2 text-xs flex justify-between">
<span>مبلغ: <strong>{{ formatWithdrawAmountDisplay(selectedDepositRecord) }}</strong></span>
<span>شناسه: <strong dir="ltr">{{ selectedDepositRecord.deposit_identifier || selectedDepositRecord.customer?.n_code || '-' }}</strong></span>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">علت رد درخواست (در پیامک برای مشتری ارسال می‌شود):</label>
<a-textarea
v-model:value="rejectReason"
:rows="3"
placeholder="مثال: عدم تطابق فیش واریزی با گردش حساب بانکی..."
/>
</div>
<div class="text-xs text-gray-500">
با رد واریز، پیامک اطلاع‌رسانی حاوی علت رد برای مشتری ارسال می‌شود.
</div>
</div>
</BasicModal>
</div>
</template>
@@ -114,10 +205,21 @@ import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import type { Order } from '@/model/order'
import type { Wallet, Withdraw } from '@/model/wallet'
import {completeWithdraw, exportWithdrawReport, getWithdrawById, sendWeights} from '@/api/wallet'
import {
approveDeposit,
completeWithdraw,
exportWithdrawReport,
getWithdrawById,
rejectDeposit,
sendWeights
} from '@/api/wallet'
import Button from "@core/components/BaseButton";
import type { WithdrawFlowType } from '@/pages/menuPages/withdraw/types'
import { confirmLabelForRecord, needsWeightAssignment } from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
import {
confirmLabelForRecord,
formatWithdrawAmountDisplay,
needsWeightAssignment
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
import { openHtmlPrintWindow } from '@/pages/menuPages/withdraw/utils/openHtmlPrintWindow'
//import DatePicker from '@core/components/_sample/components/datePicker.vue'
@@ -360,6 +462,65 @@ function ok(i) {
}
}
// Deposit approval / rejection logic
const approveDepositModalVisible = ref(false)
const rejectDepositModalVisible = ref(false)
const selectedDepositRecord = ref<Withdraw | null>(null)
const approveTrackedNumber = ref('')
const rejectReason = ref('')
const depositActionLoading = ref(false)
function openApproveDeposit(record: Withdraw) {
selectedDepositRecord.value = record
approveTrackedNumber.value = record.tracked_number || ''
approveDepositModalVisible.value = true
}
function openRejectDeposit(record: Withdraw) {
selectedDepositRecord.value = record
rejectReason.value = ''
rejectDepositModalVisible.value = true
}
async function handleApproveDeposit() {
if (!selectedDepositRecord.value?.id) return
try {
depositActionLoading.value = true
await approveDeposit({
id: selectedDepositRecord.value.id,
tracked_number: approveTrackedNumber.value || undefined
})
message.success('واریز با موفقیت تایید و به کیف پول مشتری افزوده شد')
approveDepositModalVisible.value = false
selectedDepositRecord.value = null
refreshTable()
} catch (e: any) {
console.error(e)
message.error(e?.response?.data?.message || 'خطا در تایید واریز')
} finally {
depositActionLoading.value = false
}
}
async function handleRejectDeposit() {
if (!selectedDepositRecord.value?.id) return
try {
depositActionLoading.value = true
await rejectDeposit({
id: selectedDepositRecord.value.id,
reject_reason: rejectReason.value || undefined
})
message.success('درخواست واریز رد شد و پیامک اطلاع‌رسانی ارسال گردید')
rejectDepositModalVisible.value = false
selectedDepositRecord.value = null
refreshTable()
} catch (e: any) {
console.error(e)
message.error(e?.response?.data?.message || 'خطا در رد واریز')
} finally {
depositActionLoading.value = false
}
}
</script>
<style scoped></style>
@@ -98,6 +98,15 @@ export function confirmLabelForRecord(
return flowType === 'deposit' ? 'تایید واریز' : 'تایید برداشت'
}
/** بررسی درخواست واریز معلق که نیاز به تایید یا رد ادمین دارد */
export function isPendingDepositRequest(
record: Pick<Withdraw, 'sts' | 'receipt' | 'receipt_url' | 'deposit_identifier' | 'note' | 'cash' | 'gold' | 'bank_account_id' | 'type'>
): boolean {
if (Number(record.sts ?? 0) !== 1) return false
if (record.receipt_url || record.receipt || record.deposit_identifier) return true
return resolveRecordFlowType(record as any) === 'deposit'
}
/** فقط تراکنش‌های در انتظار پیگیری — نه واریز/کسر ادمین (sts=4) */
export function needsTrackingConfirmation(record: Pick<Withdraw, 'sts' | 'tracked_number'>): boolean {
if (record.tracked_number) return false