Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
228 lines
6.9 KiB
TypeScript
228 lines
6.9 KiB
TypeScript
'use client'
|
|
|
|
import type { PaginationListColumnType } from '@/types'
|
|
import PaginatedList from '@/components/PaginatedList'
|
|
import PageNavbar from '@/components/layouts/PageNavbar'
|
|
import Button from '@/components/formElements/Button'
|
|
import EyeCrossedIcon from '@/components/icons/EyeCrossedIcon'
|
|
import FileCheckIcon from '@/components/icons/FileCheckIcon'
|
|
import TrashIcon from '@/components/icons/TrashIcon'
|
|
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 useAlertModal from '@/hooks/useAlertModal'
|
|
import useAdminMutation from '@/hooks/useAdminMutation'
|
|
import { formatPersonName, coerceToString } from '@/helpers'
|
|
import { formatPersianDate, truncateValue } from '@/lib/formatters'
|
|
import { getReviewStatus, REVIEW_STATUS_FILTER_ITEMS, type ReviewStatus } from '@/constants/status'
|
|
import { API_ROUTES } from '@/services/config'
|
|
|
|
const columns: PaginationListColumnType[] = [
|
|
{
|
|
field: 'eventId',
|
|
label: 'رویداد',
|
|
filterable: true,
|
|
type: 'text',
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'userId',
|
|
label: 'نویسنده',
|
|
filterable: false,
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'rating',
|
|
label: 'امتیاز',
|
|
filterable: true,
|
|
type: 'number',
|
|
sortable: true,
|
|
},
|
|
{
|
|
field: 'status',
|
|
label: 'وضعیت',
|
|
filterable: true,
|
|
type: 'select',
|
|
sortable: false,
|
|
filterItems: REVIEW_STATUS_FILTER_ITEMS,
|
|
},
|
|
{
|
|
field: 'createdAt',
|
|
label: 'تاریخ ثبت',
|
|
filterable: true,
|
|
sortable: true,
|
|
type: 'dateFromTo',
|
|
},
|
|
{
|
|
field: 'body',
|
|
label: 'متن نظر',
|
|
filterable: false,
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'actions',
|
|
label: 'عملیات',
|
|
},
|
|
]
|
|
|
|
interface ReviewEventSummary {
|
|
id: string
|
|
title: string
|
|
}
|
|
|
|
interface ReviewUserSummary {
|
|
id: string
|
|
firstName: string | null
|
|
lastName: string | null
|
|
}
|
|
|
|
interface ReviewRow {
|
|
id: string
|
|
eventId: string
|
|
userId: string
|
|
status: ReviewStatus
|
|
event?: ReviewEventSummary
|
|
user?: ReviewUserSummary
|
|
[key: string]: unknown
|
|
}
|
|
|
|
const getEventSummary = (row: ReviewRow) => {
|
|
if (row.event && typeof row.event === 'object') {
|
|
return row.event
|
|
}
|
|
|
|
return { id: row.eventId, title: '—' }
|
|
}
|
|
|
|
const getUserSummary = (row: ReviewRow) => {
|
|
if (row.user && typeof row.user === 'object') {
|
|
return row.user
|
|
}
|
|
|
|
return { id: row.userId, firstName: null, lastName: null }
|
|
}
|
|
|
|
const ReviewsPage = () => {
|
|
const { showAlert } = useAlertModal()
|
|
const { pendingId, runAction } = useAdminMutation({ url: API_ROUTES.REVIEWS.ADMIN_LIST })
|
|
|
|
const handleHide = (row: ReviewRow) => {
|
|
showAlert('این نظر از نمایش عمومی مخفی شود؟', () =>
|
|
runAction(row.id, () => axiosInstance.patch(API_ROUTES.REVIEWS.ADMIN_HIDE(row.id)), 'نظر مخفی شد')
|
|
)
|
|
}
|
|
|
|
const handleRestore = (row: ReviewRow) => {
|
|
void runAction(row.id, () => axiosInstance.patch(API_ROUTES.REVIEWS.ADMIN_RESTORE(row.id)), 'نظر بازگردانده شد')
|
|
}
|
|
|
|
const handleDelete = (row: ReviewRow) => {
|
|
showAlert(
|
|
'این نظر برای همیشه حذف شود؟ این عملیات قابل بازگشت نیست.',
|
|
() => runAction(row.id, () => axiosInstance.delete(API_ROUTES.REVIEWS.ADMIN_DELETE(row.id)), 'نظر حذف شد'),
|
|
undefined,
|
|
{ dangerAccept: true }
|
|
)
|
|
}
|
|
|
|
return (
|
|
<section className="h-full w-full text-right">
|
|
<PageNavbar pageTitle="مدیریت نظرات" />
|
|
<div className="admin-page-container">
|
|
<PaginatedList
|
|
columns={columns}
|
|
url={API_ROUTES.REVIEWS.ADMIN_LIST}
|
|
>
|
|
{{
|
|
eventId: (row) => getEventSummary(row as ReviewRow).title,
|
|
userId: (row) => {
|
|
const user = getUserSummary(row as ReviewRow)
|
|
|
|
return formatPersonName(user.firstName, user.lastName)
|
|
},
|
|
body: (_row, cellValue) => truncateValue(cellValue),
|
|
status: (_row, cellValue) => {
|
|
const { label, chipColor } = getReviewStatus(coerceToString(cellValue))
|
|
|
|
return (
|
|
<StatusChip
|
|
chipColor={chipColor}
|
|
label={label}
|
|
/>
|
|
)
|
|
},
|
|
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
|
actions: (row) => {
|
|
const review = row as ReviewRow
|
|
const isBusy = pendingId === review.id
|
|
const event = getEventSummary(review)
|
|
|
|
return (
|
|
<AdminTableActions>
|
|
<AdminTableViewButton
|
|
label="مشاهده رویداد"
|
|
mode="navigate"
|
|
to={APP_ROUTES.MANAGE_EVENT_DETAIL(event.id)}
|
|
/>
|
|
{review.status !== 'deleted' && review.status === 'published' ? (
|
|
<Button
|
|
iconOnly
|
|
aria-label="مخفی کردن نظر"
|
|
color="warning"
|
|
disabled={isBusy}
|
|
isLoading={isBusy}
|
|
size="sm"
|
|
variant="flat"
|
|
onClick={() => {
|
|
handleHide(review)
|
|
}}
|
|
>
|
|
<EyeCrossedIcon className="size-4" />
|
|
</Button>
|
|
) : null}
|
|
{review.status !== 'deleted' && review.status === 'hidden' ? (
|
|
<Button
|
|
iconOnly
|
|
aria-label="بازگردانی نظر"
|
|
color="success"
|
|
disabled={isBusy}
|
|
isLoading={isBusy}
|
|
size="sm"
|
|
variant="flat"
|
|
onClick={() => {
|
|
handleRestore(review)
|
|
}}
|
|
>
|
|
<FileCheckIcon className="size-4" />
|
|
</Button>
|
|
) : null}
|
|
{review.status !== 'deleted' ? (
|
|
<Button
|
|
iconOnly
|
|
aria-label="حذف نظر"
|
|
color="danger"
|
|
disabled={isBusy}
|
|
isLoading={isBusy}
|
|
size="sm"
|
|
variant="flat"
|
|
onClick={() => {
|
|
handleDelete(review)
|
|
}}
|
|
>
|
|
<TrashIcon className="size-4 text-fourth-900" />
|
|
</Button>
|
|
) : null}
|
|
</AdminTableActions>
|
|
)
|
|
},
|
|
}}
|
|
</PaginatedList>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default ReviewsPage
|