'use client' import { useCallback, useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' 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 EyeIcon from '@/components/icons/EyeIcon' 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 { getIdentityStatus, IDENTITY_VERIFICATION_QUEUE_FILTER_ITEMS } from '@/constants/status' import { formatPersonName, coerceToString } from '@/helpers' import { unwrapApiPayload } from '@/helpers/listResponse' import useAlertModal from '@/hooks/useAlertModal' import useAdminMutation from '@/hooks/useAdminMutation' import { formatPersianDate } from '@/lib/formatters' import { API_ROUTES } from '@/services/config' type IdentityVerificationStatus = 'pending' | 'verified' | 'rejected' interface RejectionReason { id: number code: string label: string sortOrder: number } interface VerificationUserSummary { mobile: string firstName: string | null lastName: string | null } interface VerificationRow { id: string userId: string status: IdentityVerificationStatus nationalCode: string | null contractVersion?: string | null contractAcceptedAt?: string | null contractAcceptanceId?: string | null jibitMatched?: boolean | null jibitInquiredAt?: string | null jibitErrorCode?: string | null reason?: { code: string; label: string } | null rejectionReason?: string | null user?: VerificationUserSummary createdAt: string reviewedAt?: string | null [key: string]: unknown } interface RejectFormValues { reasonId: string freeTextReason: string } const REJECT_FORM_DEFAULTS: RejectFormValues = { reasonId: '', freeTextReason: '', } const columns: PaginationListColumnType[] = [ { field: 'userId', label: 'متقاضی', filterable: true, type: 'text', sortable: false, }, { field: 'nationalCode', label: 'کد ملی', filterable: false, sortable: false, }, { field: 'jibitMatched', label: 'استعلام جیبت', filterable: false, sortable: false, }, { field: 'status', label: 'وضعیت', filterable: true, type: 'select', sortable: true, filterItems: IDENTITY_VERIFICATION_QUEUE_FILTER_ITEMS, }, { field: 'createdAt', label: 'تاریخ ثبت', filterable: true, sortable: true, type: 'dateFromTo', }, { field: 'actions', label: 'عملیات', }, ] const formatJibitCell = (row: VerificationRow) => { if (row.jibitErrorCode) return `خطا: ${row.jibitErrorCode}` if (row.jibitMatched === true) return 'تطبیق دارد' if (row.jibitMatched === false) return 'تطبیق ندارد' return '—' } const getUserSummary = (row: VerificationRow): VerificationUserSummary & { id: string } => { const user = row.user return { id: row.userId, mobile: user?.mobile ?? '—', firstName: user?.firstName ?? null, lastName: user?.lastName ?? null, } } const IdentityVerificationsPage = () => { const { showAlert } = useAlertModal() const { pendingId, runAction } = useAdminMutation({ url: API_ROUTES.IDENTITY.ADMIN_LIST }) const [detailTarget, setDetailTarget] = useState(null) const [rejectTarget, setRejectTarget] = useState(null) const [rejectionReasons, setRejectionReasons] = useState([]) const isRejecting = rejectTarget?.id === pendingId const rejectForm = useForm({ defaultValues: REJECT_FORM_DEFAULTS, }) const selectedReasonId = rejectForm.watch('reasonId') const freeTextReason = rejectForm.watch('freeTextReason') const loadRejectionReasons = useCallback(async () => { try { const response = await axiosInstance.get(API_ROUTES.IDENTITY.REJECTION_REASONS) const payload = unwrapApiPayload(response.data) const reasons = Array.isArray(payload) ? payload : [] setRejectionReasons(reasons as RejectionReason[]) } catch { addToast({ title: 'بارگذاری دلایل رد ناموفق بود', color: 'danger' }) } }, []) useEffect(() => { if (rejectTarget) { void loadRejectionReasons() } }, [rejectTarget, loadRejectionReasons]) const handleApprove = (row: VerificationRow) => { showAlert('این درخواست احراز هویت تأیید شود؟', () => runAction(row.id, () => axiosInstance.patch(API_ROUTES.IDENTITY.ADMIN_APPROVE(row.id)), 'درخواست تأیید شد') ) } const handleJibitInquiry = (row: VerificationRow) => { showAlert('استعلام تطابق کدملی با موبایل از جیبت انجام شود؟', () => runAction( row.id, async () => { const response = await axiosInstance.post(API_ROUTES.IDENTITY.ADMIN_JIBIT_INQUIRY(row.id)) const payload = unwrapApiPayload>(response.data) ?? {} setDetailTarget({ ...row, ...payload, user: payload.user ?? row.user, }) return response }, 'استعلام جیبت انجام شد' ) ) } const openRejectModal = (row: VerificationRow) => { rejectForm.reset(REJECT_FORM_DEFAULTS) setRejectTarget(row) } const closeRejectModal = () => { if (isRejecting) return setRejectTarget(null) rejectForm.reset(REJECT_FORM_DEFAULTS) } const handleReject = async () => { if (!rejectTarget) return const reasonId = selectedReasonId ? Number(selectedReasonId) : undefined const rejectionReason = freeTextReason.trim() || undefined if (!reasonId && !rejectionReason) { addToast({ title: 'حداقل یکی از دلیل ساختاریافته یا توضیح آزاد را وارد کنید', color: 'warning' }) return } const succeeded = await runAction( rejectTarget.id, () => axiosInstance.patch(API_ROUTES.IDENTITY.ADMIN_REJECT(rejectTarget.id), { ...(reasonId ? { reasonId } : {}), ...(rejectionReason ? { rejectionReason } : {}), }), 'درخواست رد شد' ) if (succeeded) { setRejectTarget(null) rejectForm.reset(REJECT_FORM_DEFAULTS) } } const renderStatusCell = (row: VerificationRow, cellValue: unknown) => { const { label, chipColor } = getIdentityStatus(coerceToString(cellValue)) const description = row.status === 'rejected' && (row.reason?.label || row.rejectionReason) ? `${row.reason?.label ?? ''}${row.reason?.label && row.rejectionReason ? ' — ' : ''}${row.rejectionReason ?? ''}` : undefined return ( ) } return (
{{ userId: (row) => { const user = getUserSummary(row as VerificationRow) return (
{formatPersonName(user.firstName, user.lastName)} {user.mobile}
) }, jibitMatched: (row) => formatJibitCell(row as VerificationRow), status: (row, cellValue) => renderStatusCell(row as VerificationRow, cellValue), createdAt: (_row, cellValue) => formatPersianDate(cellValue), actions: (row) => { const verification = row as VerificationRow const isBusy = pendingId === verification.id const user = getUserSummary(verification) return ( {verification.status === 'pending' ? ( <> ) : null} ) }, }}
) : undefined } hideFooter={detailTarget?.status !== 'pending'} isDismissable={detailTarget ? pendingId !== detailTarget.id : true} isOpen={Boolean(detailTarget)} size="2xl" title="جزئیات احراز هویت" onOpenChange={(open) => { if (!open && pendingId !== detailTarget?.id) setDetailTarget(null) }} > {detailTarget ? (
متقاضی
{formatPersonName(detailTarget.user?.firstName, detailTarget.user?.lastName)}
شماره موبایل
{detailTarget.user?.mobile ?? '—'}
کد ملی
{detailTarget.nationalCode ?? '—'}
شناسه برگزارکننده
{detailTarget.userId}
نسخه قرارداد
{detailTarget.contractVersion ?? '—'}
تاریخ پذیرش
{detailTarget.contractAcceptedAt ? formatPersianDate(detailTarget.contractAcceptedAt) : '—'}
شناسه پذیرش الکترونیکی
{detailTarget.contractAcceptanceId ?? '—'}
نتیجه استعلام جیبت
{formatJibitCell(detailTarget)}
زمان استعلام
{detailTarget.jibitInquiredAt ? formatPersianDate(detailTarget.jibitInquiredAt) : '—'}
) : null}
{ void handleReject() }} onOpenChange={(open) => { if (!open) closeRejectModal() }} onReject={closeRejectModal} >

حداقل یکی از «دلیل ساختاریافته» یا «توضیح آزاد» را وارد کنید. این اطلاعات برای کاربر نمایش داده می‌شود.

) } export default IdentityVerificationsPage