admin/app/(dashboard)/bank-accounts/page.tsx
alisaza 76e65f4f79 refactor: improve code readability and validation structure
- Added a new line for better readability in the BankAccountsPage component when showing alert messages.
- Refactored the test case for isRequestHostAllowed to enhance clarity.
- Improved formatting in the CreateBankAccountFormValidation schema for the nationalCode field.
2026-09-05 16:31:02 +03:30

304 lines
9.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 Button from '@/components/formElements/Button'
import Input from '@/components/formElements/Input'
import CloseCircleIcon from '@/components/icons/CloseCircleIcon'
import FileCheckIcon from '@/components/icons/FileCheckIcon'
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 {
BANK_ACCOUNT_VERIFICATION_STATUS_FILTER_ITEMS,
getBankAccountVerificationStatus,
type BankAccountVerificationStatus,
} from '@/constants/status'
import { formatPersonName, coerceToString } from '@/helpers'
import useAlertModal from '@/hooks/useAlertModal'
import useAdminMutation from '@/hooks/useAdminMutation'
import { formatPersianDate } from '@/lib/formatters'
import { API_ROUTES } from '@/services/config'
interface BankAccountUserSummary {
mobile: string
firstName: string | null
lastName: string | null
}
interface BankAccountRow {
id: string
userId: string
ownerType: 'guest' | 'organizer'
iban: string
nationalCode: string | null
verificationStatus: BankAccountVerificationStatus
rejectionReason: string | null
jibitMatched: boolean | null
jibitInquiredAt: string | null
jibitErrorCode: string | null
createdAt: string
user?: BankAccountUserSummary
[key: string]: unknown
}
const columns: PaginationListColumnType[] = [
{
field: 'userId',
label: 'کاربر',
filterable: true,
sortable: false,
type: 'text',
},
{
field: 'ownerType',
label: 'نوع',
filterable: true,
sortable: false,
type: 'select',
filterItems: [
{ code: 'guest', name: 'مهمان' },
{ code: 'organizer', name: 'میزبان' },
],
},
{
field: 'iban',
label: 'شبا',
filterable: false,
sortable: false,
},
{
field: 'nationalCode',
label: 'کد ملی',
filterable: false,
sortable: false,
},
{
field: 'jibitMatched',
label: 'استعلام جیبت',
filterable: false,
sortable: false,
},
{
field: 'verificationStatus',
label: 'وضعیت',
filterable: true,
sortable: true,
type: 'select',
filterItems: BANK_ACCOUNT_VERIFICATION_STATUS_FILTER_ITEMS,
},
{
field: 'createdAt',
label: 'تاریخ ثبت',
filterable: false,
sortable: true,
type: 'date',
},
{
field: 'actions',
label: 'عملیات',
},
]
const BankAccountsPage = () => {
const { showAlert } = useAlertModal()
const { pendingId, runAction } = useAdminMutation({ url: API_ROUTES.BANK_ACCOUNTS.ADMIN_LIST })
const [rejectTarget, setRejectTarget] = useState<BankAccountRow | null>(null)
const [rejectReason, setRejectReason] = useState('')
const isRejecting = rejectTarget?.id === pendingId
const handleInquire = (row: BankAccountRow) => {
showAlert('استعلام تطبیق شبا از جیبیت انجام شود؟', () =>
runAction(row.id, () => axiosInstance.patch(API_ROUTES.BANK_ACCOUNTS.ADMIN_JIBIT_INQUIRY(row.id)), 'نتیجه استعلام جیبیت ذخیره شد')
)
}
const handleApprove = (row: BankAccountRow) => {
if (row.jibitMatched !== true) {
addToast({ title: 'تأیید فقط پس از استعلام موفق جیبیت (matched) ممکن است', color: 'warning' })
return
}
const userName = formatPersonName(row.user?.firstName, row.user?.lastName) || row.iban
showAlert(`حساب «${userName}» تأیید شود؟`, () =>
runAction(row.id, () => axiosInstance.patch(API_ROUTES.BANK_ACCOUNTS.ADMIN_APPROVE(row.id)), 'حساب بانکی تأیید شد')
)
}
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.BANK_ACCOUNTS.ADMIN_REJECT(rejectTarget.id), { reason }),
'حساب بانکی رد شد'
)
if (succeeded) {
setRejectTarget(null)
setRejectReason('')
}
}
return (
<section className="h-full w-full text-right">
<PageNavbar pageTitle="حساب‌های بانکی" />
<div className="admin-page-container">
<PaginatedList
columns={columns}
url={API_ROUTES.BANK_ACCOUNTS.ADMIN_LIST}
>
{{
userId: (row) => {
const account = row as BankAccountRow
return (
<div className="flex flex-col gap-1">
<span>{formatPersonName(account.user?.firstName, account.user?.lastName)}</span>
{account.user?.mobile ? <span className="text-text-muted text-xs">{account.user.mobile}</span> : null}
</div>
)
},
ownerType: (_row, cellValue) => (coerceToString(cellValue) === 'organizer' ? 'میزبان' : 'مهمان'),
iban: (_row, cellValue) => (
<span
className="font-mono text-xs"
dir="ltr"
>
{coerceToString(cellValue)}
</span>
),
jibitMatched: (row) => {
const account = row as BankAccountRow
if (account.jibitErrorCode) return <span className="text-fourth-900 text-xs">{account.jibitErrorCode}</span>
if (account.jibitMatched === true) return <span className="text-fifth text-xs">مطابق</span>
if (account.jibitMatched === false) return <span className="text-fourth-900 text-xs">عدم تطابق</span>
return <span className="text-text-muted text-xs">استعلام نشده</span>
},
verificationStatus: (row, cellValue) => {
const account = row as BankAccountRow
const { label, chipColor } = getBankAccountVerificationStatus(coerceToString(cellValue))
return (
<StatusChip
chipColor={chipColor}
description={account.verificationStatus === 'rejected' ? (account.rejectionReason ?? undefined) : undefined}
label={label}
/>
)
},
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
actions: (row) => {
const account = row as BankAccountRow
const isBusy = pendingId === account.id
const isPending = account.verificationStatus === 'pending_review'
return (
<AdminTableActions>
<AdminTableViewButton
label="مشاهده کاربر"
mode="navigate"
to={APP_ROUTES.USER_DETAIL(account.userId)}
/>
{isPending ? (
<>
<Button
color="primary"
disabled={isBusy}
isLoading={isBusy}
size="sm"
variant="flat"
onClick={() => {
handleInquire(account)
}}
>
استعلام
</Button>
<Button
iconOnly
aria-label="تأیید حساب بانکی"
color="success"
disabled={isBusy || account.jibitMatched !== true}
isLoading={isBusy}
size="sm"
variant="flat"
onClick={() => {
handleApprove(account)
}}
>
<FileCheckIcon className="size-4" />
</Button>
<Button
iconOnly
aria-label="رد حساب بانکی"
color="danger"
disabled={isBusy}
isLoading={isBusy}
size="sm"
variant="flat"
onClick={() => {
setRejectReason('')
setRejectTarget(account)
}}
>
<CloseCircleIcon className="size-4 text-fourth-900" />
</Button>
</>
) : null}
</AdminTableActions>
)
},
}}
</PaginatedList>
</div>
<Modal
acceptBtnText="رد حساب"
isLoading={isRejecting}
isOpen={Boolean(rejectTarget)}
rejectBtnText="انصراف"
title="رد حساب بانکی"
onAccept={() => {
void handleReject()
}}
onOpenChange={(open) => {
if (!open && !isRejecting) {
setRejectTarget(null)
setRejectReason('')
}
}}
>
<Input
generalType="textarea"
label="دلیل رد"
name="reason"
value={rejectReason}
onValueChange={(next) => {
setRejectReason(coerceToString(next))
}}
/>
</Modal>
</section>
)
}
export default BankAccountsPage