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

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