admin/app/(dashboard)/reviews/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

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