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.
453 lines
17 KiB
TypeScript
453 lines
17 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 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
|