admin/app/(dashboard)/withdrawal-requests/page.tsx
2026-09-05 16:04:36 +03:30

345 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState } from 'react'
import type { PaginationListColumnType } from '@/types'
import { addToast } from '@/lib/toast'
import PaginatedList from '@/components/PaginatedList'
import PageNavbar from '@/components/layouts/PageNavbar'
import Input from '@/components/formElements/Input'
import Button from '@/components/formElements/Button'
import CloseCircleIcon from '@/components/icons/CloseCircleIcon'
import FileCheckIcon from '@/components/icons/FileCheckIcon'
import ManualPayoutModal from '@/components/admin/ManualPayoutModal'
import PlayCircleIcon from '@/components/icons/PlayCircleIcon'
import Modal from '@/components/modals/Modal'
import AdminTableActions from '@/components/ui/AdminTableActions'
import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
import StatusChip from '@/components/ui/StatusChip'
import axiosInstance from '@/config/axios'
import { APP_ROUTES } from '@/constants/routes'
import { getWithdrawalRequestStatus, WITHDRAWAL_REQUEST_STATUS_FILTER_ITEMS } from '@/constants/status'
import { formatCurrency, formatPersonName, coerceToString } from '@/helpers'
import useAlertModal from '@/hooks/useAlertModal'
import useAdminMutation from '@/hooks/useAdminMutation'
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
import { API_ROUTES } from '@/services/config'
type WithdrawalRequestStatusValue = 'pending' | 'processing' | 'completed' | 'rejected'
interface WithdrawalRequestUserSummary {
mobile: string
firstName: string | null
lastName: string | null
}
interface WithdrawalRequestBankAccountSummary {
iban: string
}
interface WithdrawalRequestRow {
id: string
userId: string
walletId: string
bankAccountId: string
withdrawalCode: string
amount: number
status: WithdrawalRequestStatusValue
rejectionReason: string | null
processedAt: string | null
createdAt: string
updatedAt: string
user?: WithdrawalRequestUserSummary
bankAccount?: WithdrawalRequestBankAccountSummary
[key: string]: unknown
}
const columns: PaginationListColumnType[] = [
{
field: 'withdrawalCode',
label: 'کد درخواست',
filterable: false,
sortable: false,
type: 'text',
},
{
field: 'userId',
label: 'کاربر',
filterable: true,
sortable: false,
type: 'text',
},
{
field: 'amount',
label: 'مبلغ',
filterable: false,
sortable: true,
},
{
field: 'status',
label: 'وضعیت',
filterable: true,
sortable: true,
type: 'select',
filterItems: WITHDRAWAL_REQUEST_STATUS_FILTER_ITEMS,
},
{
field: 'createdAt',
label: 'تاریخ ثبت',
filterable: false,
sortable: true,
},
{
field: 'processedAt',
label: 'تاریخ پردازش',
filterable: false,
sortable: false,
},
{
field: 'bankDetails',
label: 'حساب بانکی',
filterable: false,
sortable: false,
},
{
field: 'actions',
label: 'عملیات',
},
]
const WithdrawalRequestsPage = () => {
const { showAlert } = useAlertModal()
const { pendingId, runAction } = useAdminMutation({ url: API_ROUTES.WITHDRAWAL_REQUESTS.ADMIN_LIST })
const [rejectTarget, setRejectTarget] = useState<WithdrawalRequestRow | null>(null)
const [manualTarget, setManualTarget] = useState<WithdrawalRequestRow | null>(null)
const [rejectReason, setRejectReason] = useState('')
const isRejecting = rejectTarget?.id === pendingId
const handleProcess = (row: WithdrawalRequestRow) => {
showAlert('این درخواست برداشت به وضعیت «در حال پردازش» تغییر کند؟', () =>
runAction(
row.id,
() => axiosInstance.patch(API_ROUTES.WITHDRAWAL_REQUESTS.ADMIN_PROCESS(row.id)),
'درخواست به «در حال پردازش» تغییر کرد'
)
)
}
const openRejectModal = (row: WithdrawalRequestRow) => {
setRejectReason('')
setRejectTarget(row)
}
const closeRejectModal = () => {
if (isRejecting) return
setRejectTarget(null)
setRejectReason('')
}
const handleReject = async () => {
if (!rejectTarget) return
const reason = rejectReason.trim()
if (reason.length < 3) {
addToast({ title: 'دلیل رد باید حداقل ۳ کاراکتر باشد', color: 'warning' })
return
}
const succeeded = await runAction(
rejectTarget.id,
() => axiosInstance.patch(API_ROUTES.WITHDRAWAL_REQUESTS.ADMIN_REJECT(rejectTarget.id), { reason }),
'درخواست رد شد'
)
if (succeeded) {
setRejectTarget(null)
setRejectReason('')
}
}
const renderStatusCell = (row: WithdrawalRequestRow, cellValue: unknown) => {
const { label, chipColor } = getWithdrawalRequestStatus(coerceToString(cellValue))
return (
<StatusChip
chipColor={chipColor}
description={row.status === 'rejected' ? row.rejectionReason : undefined}
label={label}
/>
)
}
return (
<section className="h-full w-full text-right">
<PageNavbar pageTitle="درخواست‌های برداشت" />
<div className="admin-page-container">
<PaginatedList
columns={columns}
url={API_ROUTES.WITHDRAWAL_REQUESTS.ADMIN_LIST}
>
{{
userId: (row) => {
const withdrawal = row as WithdrawalRequestRow
return (
<div className="flex flex-col gap-1">
<span>{formatPersonName(withdrawal.user?.firstName, withdrawal.user?.lastName)}</span>
{withdrawal.user?.mobile ? (
<span
className="text-text-muted text-xs"
dir="ltr"
>
{formatIranianMobile(withdrawal.user.mobile)}
</span>
) : null}
</div>
)
},
amount: (_row, cellValue) => formatCurrency(Number(cellValue ?? 0)),
status: (row, cellValue) => renderStatusCell(row as WithdrawalRequestRow, cellValue),
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
processedAt: (_row, cellValue) => formatPersianDate(cellValue),
bankDetails: (row) => {
const withdrawal = row as WithdrawalRequestRow
const bankAccount = withdrawal.bankAccount
if (!bankAccount) {
return <span className="text-text-muted text-xs"></span>
}
return (
<span
className="text-xs font-mono"
dir="ltr"
>
{bankAccount.iban || '—'}
</span>
)
},
actions: (row) => {
const withdrawal = row as WithdrawalRequestRow
const isBusy = pendingId === withdrawal.id
const canProcess = withdrawal.status === 'pending' || withdrawal.status === 'processing'
return (
<AdminTableActions>
<AdminTableViewButton
label="مشاهده کاربر"
mode="navigate"
to={APP_ROUTES.USER_DETAIL(withdrawal.userId)}
/>
{canProcess && withdrawal.status === 'pending' ? (
<Button
iconOnly
aria-label="پردازش درخواست برداشت"
color="primary"
disabled={isBusy}
isLoading={isBusy}
size="sm"
variant="flat"
onClick={() => {
handleProcess(withdrawal)
}}
>
<PlayCircleIcon
className="size-4"
color="currentColor"
/>
</Button>
) : null}
{canProcess ? (
<>
<Button
iconOnly
aria-label="تکمیل درخواست برداشت"
color="success"
disabled={isBusy}
isLoading={isBusy}
size="sm"
variant="flat"
onClick={() => {
setManualTarget(withdrawal)
}}
>
<FileCheckIcon className="size-4" />
</Button>
<Button
iconOnly
aria-label="رد درخواست برداشت"
color="danger"
disabled={isBusy}
isLoading={isBusy}
size="sm"
variant="flat"
onClick={() => {
openRejectModal(withdrawal)
}}
>
<CloseCircleIcon className="size-4 text-fourth-900" />
</Button>
</>
) : null}
</AdminTableActions>
)
},
}}
</PaginatedList>
</div>
<Modal
acceptDanger
acceptBtnDisabled={rejectReason.trim().length < 3}
acceptBtnText="رد درخواست"
isLoading={isRejecting}
isOpen={Boolean(rejectTarget)}
rejectBtnText="انصراف"
size="lg"
title="رد درخواست برداشت"
onAccept={handleReject}
onOpenChange={(open) => {
if (!open) closeRejectModal()
}}
onReject={closeRejectModal}
>
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">دلیل رد این درخواست را بنویسید. این توضیح برای کاربر نمایش داده میشود.</p>
<Input
generalType="textarea"
label="دلیل رد"
name="rejectReason"
placeholder="حداقل ۳ کاراکتر"
textAreaMinRows={3}
value={rejectReason}
onValueChange={(next) => {
setRejectReason(coerceToString(next))
}}
/>
</div>
</Modal>
<ManualPayoutModal
isLoading={manualTarget?.id === pendingId}
isOpen={Boolean(manualTarget)}
title={manualTarget ? `ثبت پرداخت «${manualTarget.withdrawalCode}»` : 'ثبت پرداخت دستی'}
onClose={() => {
setManualTarget(null)
}}
onSubmit={async (payload) => {
if (!manualTarget) return false
return runAction(
manualTarget.id,
() => axiosInstance.patch(API_ROUTES.WITHDRAWAL_REQUESTS.ADMIN_COMPLETE(manualTarget.id), payload),
'برداشت با رسید دستی تکمیل شد'
)
}}
/>
</section>
)
}
export default WithdrawalRequestsPage