admin/app/(dashboard)/users/[id]/page.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

445 lines
16 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 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