admin/app/(dashboard)/users/[id]/page.tsx
alisaza 1612e3575d feat(events): add admin UI to create events for verified hosts
Wire manage-events/new with host picker and create flow against the new
admin create API, including entry points from the events list and user detail.
2026-09-09 11:22:00 +03:30

453 lines
17 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 gap-2 pb-2">
{detail.identityStatus === 'verified' ? (
<Button
size="sm"
to={APP_ROUTES.MANAGE_EVENT_NEW_FOR_HOST(userId)}
>
ساخت رویداد برای این میزبان
</Button>
) : null}
<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