Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
445 lines
16 KiB
TypeScript
445 lines
16 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import dynamic from 'next/dynamic'
|
||
import { useParams } from 'next/navigation'
|
||
|
||
import { Tab } from '@/components/heroui/Tabs'
|
||
import useDisclosure from '@/hooks/useDisclosure'
|
||
import PaginatedList from '@/components/PaginatedList'
|
||
import PageNavbar from '@/components/layouts/PageNavbar'
|
||
import Button from '@/components/formElements/Button'
|
||
import { DetailSkeleton, TableSkeleton } from '@/components/feedback/LoadingState'
|
||
import AdminState from '@/components/feedback/AdminState'
|
||
import { APP_ROUTES } from '@/constants/routes'
|
||
import { formatPersonName, coerceToString } from '@/helpers'
|
||
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
|
||
import StatusChip from '@/components/ui/StatusChip'
|
||
import AppTabs from '@/components/ui/AppTabs'
|
||
import useAuth from '@/hooks/useAuth'
|
||
import useAlertModal from '@/hooks/useAlertModal'
|
||
import { addToast } from '@/lib/toast'
|
||
import { useAdminUserDetail } from '@/features/admin-users/useAdminUserDetail'
|
||
import { ADMIN_ATTEST_IDENTITY } from '@/services/adminUserDetail'
|
||
import {
|
||
AdminUserActivitySummary,
|
||
AdminUserContactInfo,
|
||
AdminUserOverview,
|
||
blocksColumns,
|
||
bookingsColumns,
|
||
followsColumns,
|
||
formatToman,
|
||
hostedEventsColumns,
|
||
paymentsColumns,
|
||
reportsColumns,
|
||
reviewsColumns,
|
||
} from '@/features/admin-users/adminUserDetailUi'
|
||
import {
|
||
getActiveStatus,
|
||
getBooleanStatus,
|
||
getBookingStatus,
|
||
getEventStatus,
|
||
getPaymentMethod,
|
||
getPaymentStatus,
|
||
getReportStatus,
|
||
getReviewStatus,
|
||
} from '@/constants/status'
|
||
import { API_ROUTES } from '@/services/config'
|
||
|
||
const UserEditModal = dynamic(() => import('@/app/(dashboard)/users/[id]/_components/UserEditModal'), { ssr: false })
|
||
const WalletCreditModal = dynamic(() => import('@/app/(dashboard)/users/[id]/_components/WalletCreditModal'), { ssr: false })
|
||
const UserMobileChangeModal = dynamic(() => import('@/app/(dashboard)/users/[id]/_components/UserMobileChangeModal'), { ssr: false })
|
||
|
||
const UserDetailPage = () => {
|
||
const params = useParams<{ id: string }>()
|
||
const userId = params.id
|
||
|
||
const { user: currentAdmin } = useAuth()
|
||
const currentAdminId = currentAdmin?.userId
|
||
const { data: detail, error, isLoading, refetch: refetchDetail } = useAdminUserDetail(userId)
|
||
const { showAlert } = useAlertModal()
|
||
const { isOpen: isEditOpen, onOpen: openEdit, onOpenChange: onEditOpenChange } = useDisclosure()
|
||
const { isOpen: isCreditOpen, onOpen: openCredit, onOpenChange: onCreditOpenChange } = useDisclosure()
|
||
const { isOpen: isMobileChangeOpen, onOpen: openMobileChange, onOpenChange: onMobileChangeOpenChange } = useDisclosure()
|
||
const [editModalLoaded, setEditModalLoaded] = useState(false)
|
||
const [creditModalLoaded, setCreditModalLoaded] = useState(false)
|
||
const [mobileChangeModalLoaded, setMobileChangeModalLoaded] = useState(false)
|
||
const canChangeMobile = Boolean(detail && currentAdminId && currentAdminId !== detail.id)
|
||
const canAttestIdentity = Boolean(detail && detail.identityStatus !== 'verified')
|
||
|
||
const handleOpenEdit = () => {
|
||
setEditModalLoaded(true)
|
||
openEdit()
|
||
}
|
||
|
||
const handleOpenCredit = () => {
|
||
setCreditModalLoaded(true)
|
||
openCredit()
|
||
}
|
||
|
||
const handleOpenMobileChange = () => {
|
||
setMobileChangeModalLoaded(true)
|
||
openMobileChange()
|
||
}
|
||
|
||
const handleAttestIdentity = () => {
|
||
if (!userId || !canAttestIdentity) return
|
||
|
||
showAlert('احراز هویت این کاربر بدون ارسال درخواست از طرف خودش تأیید شود؟ پس از تأیید میتواند رویداد بسازد.', async () => {
|
||
const result = await ADMIN_ATTEST_IDENTITY(userId)
|
||
|
||
if (!result.ok) return
|
||
|
||
addToast({ title: 'احراز هویت تأیید شد', color: 'success' })
|
||
await refetchDetail()
|
||
})
|
||
}
|
||
|
||
return (
|
||
<section className="h-full w-full text-right">
|
||
<PageNavbar
|
||
endSlot={
|
||
<div className="flex items-center gap-2">
|
||
{detail && (
|
||
<>
|
||
{canChangeMobile && (
|
||
<Button
|
||
color="warning"
|
||
size="sm"
|
||
variant="flat"
|
||
onClick={handleOpenMobileChange}
|
||
>
|
||
تغییر شماره
|
||
</Button>
|
||
)}
|
||
<Button
|
||
size="sm"
|
||
variant="flat"
|
||
onClick={handleOpenCredit}
|
||
>
|
||
شارژ کیفپول
|
||
</Button>
|
||
{canAttestIdentity && (
|
||
<Button
|
||
color="success"
|
||
size="sm"
|
||
variant="flat"
|
||
onClick={handleAttestIdentity}
|
||
>
|
||
تأیید احراز هویت
|
||
</Button>
|
||
)}
|
||
<Button
|
||
size="sm"
|
||
variant="solid"
|
||
onClick={handleOpenEdit}
|
||
>
|
||
ویرایش
|
||
</Button>
|
||
</>
|
||
)}
|
||
<Button
|
||
size="sm"
|
||
to={APP_ROUTES.USERS}
|
||
variant="flat"
|
||
>
|
||
بازگشت به لیست
|
||
</Button>
|
||
</div>
|
||
}
|
||
pageTitle={detail ? formatPersonName(detail.firstName, detail.lastName, 'جزئیات کاربر') : 'جزئیات کاربر'}
|
||
/>
|
||
|
||
{detail && (
|
||
<>
|
||
{editModalLoaded ? (
|
||
<UserEditModal
|
||
currentAdminId={currentAdminId}
|
||
isOpen={isEditOpen}
|
||
user={detail}
|
||
onOpenChange={onEditOpenChange}
|
||
onSuccess={() => void refetchDetail()}
|
||
/>
|
||
) : null}
|
||
{creditModalLoaded ? (
|
||
<WalletCreditModal
|
||
isOpen={isCreditOpen}
|
||
user={detail}
|
||
onOpenChange={onCreditOpenChange}
|
||
onSuccess={() => void refetchDetail()}
|
||
/>
|
||
) : null}
|
||
{canChangeMobile && mobileChangeModalLoaded ? (
|
||
<UserMobileChangeModal
|
||
isOpen={isMobileChangeOpen}
|
||
user={detail}
|
||
onOpenChange={onMobileChangeOpenChange}
|
||
onSuccess={() => void refetchDetail()}
|
||
/>
|
||
) : null}
|
||
</>
|
||
)}
|
||
|
||
<div className="admin-page-container flex flex-col gap-5">
|
||
{isLoading && (
|
||
<div className="flex flex-col gap-5">
|
||
<DetailSkeleton />
|
||
<div className="admin-surface overflow-hidden">
|
||
<TableSkeleton />
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{!isLoading && error && (
|
||
<div className="admin-surface overflow-hidden">
|
||
<AdminState
|
||
actionLabel="تلاش دوباره"
|
||
description={error.message}
|
||
title="دریافت اطلاعات کاربر ناموفق بود"
|
||
variant="error"
|
||
onAction={() => void refetchDetail()}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{!isLoading && detail && (
|
||
<>
|
||
<AdminUserOverview detail={detail} />
|
||
<AdminUserContactInfo detail={detail} />
|
||
|
||
<AppTabs
|
||
aria-label="جزئیات کاربر"
|
||
surface="admin"
|
||
>
|
||
<Tab
|
||
key="hosted-events"
|
||
title="رویدادهای میزبانیشده"
|
||
>
|
||
<div className="flex justify-end pb-2">
|
||
<Button
|
||
size="sm"
|
||
to={`${APP_ROUTES.MANAGE_EVENTS}?filters[organizerId]=${userId}`}
|
||
variant="flat"
|
||
>
|
||
مشاهده در لیست کلی
|
||
</Button>
|
||
</div>
|
||
<PaginatedList
|
||
columns={hostedEventsColumns}
|
||
itemsKey="events"
|
||
url={API_ROUTES.USERS.ADMIN_HOSTED_EVENTS(userId)}
|
||
>
|
||
{{
|
||
status: (_row, cellValue) => <StatusChip {...getEventStatus(coerceToString(cellValue))} />,
|
||
startsAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
isFree: (_row, cellValue) => <StatusChip {...getBooleanStatus(Boolean(cellValue))} />,
|
||
price: (_row, cellValue) => formatToman(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="bookings"
|
||
title="رزروها"
|
||
>
|
||
<div className="flex justify-end pb-2">
|
||
<Button
|
||
size="sm"
|
||
to={`${APP_ROUTES.BOOKINGS}?filters[userId]=${userId}`}
|
||
variant="flat"
|
||
>
|
||
مشاهده در لیست کلی
|
||
</Button>
|
||
</div>
|
||
<PaginatedList
|
||
columns={bookingsColumns}
|
||
url={API_ROUTES.USERS.ADMIN_BOOKINGS(userId)}
|
||
>
|
||
{{
|
||
status: (_row, cellValue) => <StatusChip {...getBookingStatus(coerceToString(cellValue))} />,
|
||
checkedInAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="payments"
|
||
title="پرداختها"
|
||
>
|
||
<div className="flex justify-end pb-2">
|
||
<Button
|
||
size="sm"
|
||
to={`${APP_ROUTES.PAYMENTS}?filters[userId]=${userId}`}
|
||
variant="flat"
|
||
>
|
||
مشاهده در لیست کلی
|
||
</Button>
|
||
</div>
|
||
<PaginatedList
|
||
columns={paymentsColumns}
|
||
url={API_ROUTES.USERS.ADMIN_PAYMENTS(userId)}
|
||
>
|
||
{{
|
||
method: (_row, cellValue) => <StatusChip {...getPaymentMethod(coerceToString(cellValue))} />,
|
||
status: (_row, cellValue) => <StatusChip {...getPaymentStatus(coerceToString(cellValue))} />,
|
||
totalAmount: (_row, cellValue) => formatToman(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="reviews-written"
|
||
title="نظرات نوشتهشده"
|
||
>
|
||
<PaginatedList
|
||
columns={reviewsColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_REVIEWS(userId)}?type=written`}
|
||
>
|
||
{{
|
||
status: (_row, cellValue) => <StatusChip {...getReviewStatus(coerceToString(cellValue))} />,
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="reviews-received"
|
||
title="نظرات دریافتشده"
|
||
>
|
||
<PaginatedList
|
||
columns={reviewsColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_REVIEWS(userId)}?type=received`}
|
||
>
|
||
{{
|
||
status: (_row, cellValue) => <StatusChip {...getReviewStatus(coerceToString(cellValue))} />,
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="following"
|
||
title="دنبالشوندهها"
|
||
>
|
||
<PaginatedList
|
||
columns={followsColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_FOLLOWS(userId)}?type=following`}
|
||
>
|
||
{{
|
||
mobile: (_row, cellValue) => formatIranianMobile(cellValue),
|
||
notifyNewEvents: (_row, cellValue) => <StatusChip {...getActiveStatus(Boolean(cellValue))} />,
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="followers"
|
||
title="دنبالکنندهها"
|
||
>
|
||
<PaginatedList
|
||
columns={followsColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_FOLLOWS(userId)}?type=followers`}
|
||
>
|
||
{{
|
||
mobile: (_row, cellValue) => formatIranianMobile(cellValue),
|
||
notifyNewEvents: (_row, cellValue) => <StatusChip {...getActiveStatus(Boolean(cellValue))} />,
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="reports-filed"
|
||
title="گزارشهای ثبتشده"
|
||
>
|
||
<div className="flex justify-end pb-2">
|
||
<Button
|
||
size="sm"
|
||
to={`${APP_ROUTES.USER_REPORTS}?filters[reporterId]=${userId}`}
|
||
variant="flat"
|
||
>
|
||
مشاهده در صف بررسی
|
||
</Button>
|
||
</div>
|
||
<PaginatedList
|
||
columns={reportsColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_REPORTS(userId)}?direction=filed`}
|
||
>
|
||
{{
|
||
otherUserMobile: (_row, cellValue) => formatIranianMobile(cellValue),
|
||
status: (_row, cellValue) => <StatusChip {...getReportStatus(coerceToString(cellValue))} />,
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="reports-received"
|
||
title="گزارشهای دریافتشده"
|
||
>
|
||
<div className="flex justify-end pb-2">
|
||
<Button
|
||
size="sm"
|
||
to={`${APP_ROUTES.USER_REPORTS}?filters[reportedId]=${userId}`}
|
||
variant="flat"
|
||
>
|
||
مشاهده در صف بررسی
|
||
</Button>
|
||
</div>
|
||
<PaginatedList
|
||
columns={reportsColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_REPORTS(userId)}?direction=received`}
|
||
>
|
||
{{
|
||
otherUserMobile: (_row, cellValue) => formatIranianMobile(cellValue),
|
||
status: (_row, cellValue) => <StatusChip {...getReportStatus(coerceToString(cellValue))} />,
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="blocking"
|
||
title="بلاککرده"
|
||
>
|
||
<PaginatedList
|
||
columns={blocksColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_BLOCKS(userId)}?direction=blocking`}
|
||
>
|
||
{{
|
||
otherUserMobile: (_row, cellValue) => formatIranianMobile(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
|
||
<Tab
|
||
key="blocked-by"
|
||
title="بلاکشده توسط"
|
||
>
|
||
<PaginatedList
|
||
columns={blocksColumns}
|
||
url={`${API_ROUTES.USERS.ADMIN_BLOCKS(userId)}?direction=blockedBy`}
|
||
>
|
||
{{
|
||
otherUserMobile: (_row, cellValue) => formatIranianMobile(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
}}
|
||
</PaginatedList>
|
||
</Tab>
|
||
</AppTabs>
|
||
|
||
<AdminUserActivitySummary detail={detail} />
|
||
</>
|
||
)}
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export default UserDetailPage
|