Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
273 lines
8.8 KiB
TypeScript
273 lines
8.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
import type { PaginationListColumnType } from '@/types'
|
|
import PaginatedList from '@/components/PaginatedList'
|
|
import PageNavbar from '@/components/layouts/PageNavbar'
|
|
import Button from '@/components/formElements/Button'
|
|
import CloseCircleIcon from '@/components/icons/CloseCircleIcon'
|
|
import CloseIcon from '@/components/icons/CloseIcon'
|
|
import FileCheckIcon from '@/components/icons/FileCheckIcon'
|
|
import Modal from '@/components/modals/Modal'
|
|
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 { formatIranianMobile, formatPersianDate, truncateValue } from '@/lib/formatters'
|
|
import { getReportStatus, REPORT_STATUS_FILTER_ITEMS, type ReportStatus } from '@/constants/status'
|
|
import { API_ROUTES } from '@/services/config'
|
|
|
|
const columns: PaginationListColumnType[] = [
|
|
{
|
|
field: 'reporterId',
|
|
label: 'گزارشدهنده',
|
|
filterable: true,
|
|
type: 'text',
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'reportedId',
|
|
label: 'کاربر گزارششده',
|
|
filterable: true,
|
|
type: 'text',
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'reason',
|
|
label: 'دلیل',
|
|
filterable: false,
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'description',
|
|
label: 'توضیحات',
|
|
filterable: false,
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'status',
|
|
label: 'وضعیت',
|
|
filterable: true,
|
|
type: 'select',
|
|
sortable: true,
|
|
filterItems: REPORT_STATUS_FILTER_ITEMS,
|
|
},
|
|
{
|
|
field: 'createdAt',
|
|
label: 'تاریخ ثبت',
|
|
filterable: false,
|
|
sortable: true,
|
|
type: 'dateFromTo',
|
|
},
|
|
{
|
|
field: 'actions',
|
|
label: 'عملیات',
|
|
},
|
|
]
|
|
|
|
interface ReportReasonSummary {
|
|
id: number
|
|
code: string
|
|
label: string
|
|
sortOrder: number
|
|
}
|
|
|
|
interface ReportUserSummary {
|
|
mobile: string
|
|
firstName: string | null
|
|
lastName: string | null
|
|
}
|
|
|
|
interface UserReportRow {
|
|
id: string
|
|
reporterId: string
|
|
reportedId: string
|
|
reasonId?: number | null
|
|
description?: string | null
|
|
status: ReportStatus
|
|
reviewedAt?: string | null
|
|
reviewedBy?: string | null
|
|
createdAt: string
|
|
reason?: ReportReasonSummary | null
|
|
reporter: ReportUserSummary
|
|
reported: ReportUserSummary
|
|
[key: string]: unknown
|
|
}
|
|
|
|
const UserPartyCell = ({ summary }: { summary: ReportUserSummary }) => (
|
|
<div className="flex flex-col">
|
|
<span>{formatPersonName(summary.firstName, summary.lastName)}</span>
|
|
<span className="text-text-muted text-xs">{formatIranianMobile(summary.mobile)}</span>
|
|
</div>
|
|
)
|
|
|
|
const UserReportsPage = () => {
|
|
const { showAlert } = useAlertModal()
|
|
const { pendingId, runAction } = useAdminMutation({ url: API_ROUTES.USER_REPORTS.ADMIN_LIST })
|
|
const [selectedDescription, setSelectedDescription] = useState<string | null>(null)
|
|
|
|
const handleReview = (row: UserReportRow) => {
|
|
showAlert('این گزارش بررسی و تأیید شود؟', () =>
|
|
runAction(row.id, () => axiosInstance.patch(API_ROUTES.USER_REPORTS.ADMIN_REVIEW(row.id)), 'گزارش بررسی شد')
|
|
)
|
|
}
|
|
|
|
const handleDismiss = (row: UserReportRow) => {
|
|
showAlert('این گزارش رد شود؟', () =>
|
|
runAction(row.id, () => axiosInstance.patch(API_ROUTES.USER_REPORTS.ADMIN_DISMISS(row.id)), 'گزارش رد شد')
|
|
)
|
|
}
|
|
|
|
const handleBlock = (row: UserReportRow) => {
|
|
showAlert(
|
|
'کاربر گزارششده بهنمایندگی از گزارشدهنده مسدود شود؟ این گزارش نیز بهعنوان بررسیشده علامتگذاری میشود.',
|
|
() => runAction(row.id, () => axiosInstance.post(API_ROUTES.USER_REPORTS.ADMIN_BLOCK(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.USER_REPORTS.ADMIN_LIST}
|
|
>
|
|
{{
|
|
reporterId: (row) => {
|
|
const report = row as UserReportRow
|
|
|
|
return <UserPartyCell summary={report.reporter} />
|
|
},
|
|
reportedId: (row) => {
|
|
const report = row as UserReportRow
|
|
|
|
return <UserPartyCell summary={report.reported} />
|
|
},
|
|
reason: (row) => {
|
|
const report = row as UserReportRow
|
|
|
|
return report.reason?.label ?? '—'
|
|
},
|
|
description: (row) => {
|
|
const report = row as UserReportRow
|
|
const description = report.description
|
|
|
|
if (!description || description.trim().length === 0) return '—'
|
|
|
|
return (
|
|
<Button
|
|
className="h-auto min-h-0 justify-start p-0 text-sm text-primary hover:underline"
|
|
size="sm"
|
|
variant="light"
|
|
onClick={() => {
|
|
setSelectedDescription(description)
|
|
}}
|
|
>
|
|
{truncateValue(description)}
|
|
</Button>
|
|
)
|
|
},
|
|
status: (row, cellValue) => {
|
|
const report = row as UserReportRow
|
|
const { label, chipColor } = getReportStatus(coerceToString(cellValue))
|
|
const isClosed = report.status === 'reviewed' || report.status === 'dismissed'
|
|
|
|
return (
|
|
<StatusChip
|
|
chipColor={chipColor}
|
|
description={isClosed && report.reviewedAt ? formatPersianDate(report.reviewedAt) : undefined}
|
|
label={label}
|
|
/>
|
|
)
|
|
},
|
|
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
|
actions: (row) => {
|
|
const report = row as UserReportRow
|
|
const isBusy = pendingId === report.id
|
|
|
|
return (
|
|
<AdminTableActions>
|
|
<AdminTableViewButton
|
|
label="مشاهده کاربر گزارششده"
|
|
mode="navigate"
|
|
to={APP_ROUTES.USER_DETAIL(report.reportedId)}
|
|
/>
|
|
{report.status === 'pending' ? (
|
|
<>
|
|
<Button
|
|
iconOnly
|
|
aria-label="تأیید بررسی گزارش"
|
|
color="success"
|
|
disabled={isBusy}
|
|
isLoading={isBusy}
|
|
size="sm"
|
|
variant="flat"
|
|
onClick={() => {
|
|
handleReview(report)
|
|
}}
|
|
>
|
|
<FileCheckIcon className="size-4" />
|
|
</Button>
|
|
<Button
|
|
iconOnly
|
|
aria-label="رد گزارش"
|
|
color="default"
|
|
disabled={isBusy}
|
|
isLoading={isBusy}
|
|
size="sm"
|
|
variant="flat"
|
|
onClick={() => {
|
|
handleDismiss(report)
|
|
}}
|
|
>
|
|
<CloseIcon className="size-4" />
|
|
</Button>
|
|
<Button
|
|
iconOnly
|
|
aria-label="مسدودسازی کاربر"
|
|
color="danger"
|
|
disabled={isBusy}
|
|
isLoading={isBusy}
|
|
size="sm"
|
|
variant="flat"
|
|
onClick={() => {
|
|
handleBlock(report)
|
|
}}
|
|
>
|
|
<CloseCircleIcon className="size-4 text-fourth-900" />
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
</AdminTableActions>
|
|
)
|
|
},
|
|
}}
|
|
</PaginatedList>
|
|
</div>
|
|
<Modal
|
|
hideFooter
|
|
isOpen={selectedDescription !== null}
|
|
title="توضیحات گزارش"
|
|
onClose={() => {
|
|
setSelectedDescription(null)
|
|
}}
|
|
onOpenChange={(isOpen) => {
|
|
if (!isOpen) setSelectedDescription(null)
|
|
}}
|
|
>
|
|
<p className="whitespace-pre-wrap text-sm text-text-main">{selectedDescription}</p>
|
|
</Modal>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default UserReportsPage
|