Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
|
||
import { coerceToString } from '@/helpers'
|
||
import type { PaginationListColumnType } from '@/types'
|
||
import useDisclosure from '@/hooks/useDisclosure'
|
||
import PaginatedList from '@/components/PaginatedList'
|
||
import PageNavbar from '@/components/layouts/PageNavbar'
|
||
import Button from '@/components/formElements/Button'
|
||
import Modal from '@/components/modals/Modal'
|
||
import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
|
||
import StatusChip from '@/components/ui/StatusChip'
|
||
import { APP_ROUTES } from '@/constants/routes'
|
||
import { AUDIT_LOG_METHOD_FILTER_ITEMS, getAuditLogMethod, getHttpStatusCode } from '@/constants/status'
|
||
import { formatIranianMobile, formatPersianDate, truncateValue } from '@/lib/formatters'
|
||
import { API_ROUTES } from '@/services/config'
|
||
|
||
interface AuditLogRow {
|
||
id: string
|
||
adminUserId?: string | null
|
||
adminMobile?: string | null
|
||
method: string
|
||
path: string
|
||
params: Record<string, unknown>
|
||
statusCode: number
|
||
createdAt: string
|
||
}
|
||
|
||
// Read-only admin activity trail — GET /admin/audit-logs only supports
|
||
// filtering by adminUserId/method/path and sorting by createdAt (see
|
||
// backend/src/modules/admin-audit-logs/admin-audit-logs.service.ts,
|
||
// ALLOWED_FILTER_KEYS / ALLOWED_SORT_FIELDS). Columns that aren't
|
||
// whitelisted there (params, statusCode) are shown but
|
||
// not marked filterable/sortable, so the UI never implies a capability
|
||
// the API doesn't have.
|
||
const columns: PaginationListColumnType[] = [
|
||
{
|
||
field: 'adminUserId',
|
||
label: 'ادمین',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'text',
|
||
},
|
||
{
|
||
field: 'method',
|
||
label: 'متد',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'select',
|
||
filterItems: AUDIT_LOG_METHOD_FILTER_ITEMS,
|
||
},
|
||
{
|
||
field: 'path',
|
||
label: 'مسیر',
|
||
filterable: true,
|
||
type: 'text',
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'params',
|
||
label: 'پارامترها',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'statusCode',
|
||
label: 'کد وضعیت',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'createdAt',
|
||
label: 'زمان',
|
||
filterable: false,
|
||
sortable: true,
|
||
},
|
||
{
|
||
field: 'actions',
|
||
label: 'عملیات',
|
||
},
|
||
]
|
||
|
||
const AuditLogsPage = () => {
|
||
const { isOpen, onOpenChange, onOpen } = useDisclosure()
|
||
const [activeParams, setActiveParams] = useState<Record<string, unknown> | null>(null)
|
||
|
||
const openParamsModal = (params: Record<string, unknown>) => {
|
||
setActiveParams(params)
|
||
onOpen()
|
||
}
|
||
|
||
return (
|
||
<section className="h-full w-full text-right">
|
||
<PageNavbar pageTitle="گزارش فعالیتهای ادمین" />
|
||
<div className="admin-page-container">
|
||
<PaginatedList
|
||
columns={columns}
|
||
url={API_ROUTES.AUDIT_LOGS.ADMIN_LIST}
|
||
>
|
||
{{
|
||
adminUserId: (row) => {
|
||
const log = row as unknown as AuditLogRow
|
||
|
||
if (!log.adminUserId) {
|
||
return <span className="text-text-muted text-xs">—</span>
|
||
}
|
||
|
||
return log.adminMobile ? (
|
||
<span
|
||
className="text-text-muted text-xs"
|
||
dir="ltr"
|
||
>
|
||
{formatIranianMobile(log.adminMobile)}
|
||
</span>
|
||
) : (
|
||
<span className="text-text-muted text-xs">ادمین</span>
|
||
)
|
||
},
|
||
method: (_row, cellValue) => <StatusChip {...getAuditLogMethod(coerceToString(cellValue))} />,
|
||
path: (_row, cellValue) => (
|
||
<span
|
||
className="font-mono text-xs"
|
||
dir="ltr"
|
||
>
|
||
{coerceToString(cellValue, '—')}
|
||
</span>
|
||
),
|
||
params: (row) => {
|
||
const log = row as unknown as AuditLogRow
|
||
const serialized = JSON.stringify(log.params ?? {})
|
||
|
||
if (!log.params || Object.keys(log.params).length === 0) {
|
||
return <span className="text-text-muted text-xs">—</span>
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-1">
|
||
<span
|
||
className="font-mono text-xs"
|
||
dir="ltr"
|
||
>
|
||
{truncateValue(serialized)}
|
||
</span>
|
||
{serialized.length > 80 && (
|
||
<Button
|
||
className="w-fit"
|
||
size="sm"
|
||
variant="light"
|
||
onClick={() => {
|
||
openParamsModal(log.params)
|
||
}}
|
||
>
|
||
نمایش کامل
|
||
</Button>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
statusCode: (_row, cellValue) => <StatusChip {...getHttpStatusCode(Number(cellValue ?? 0))} />,
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
actions: (row) => {
|
||
const log = row as unknown as AuditLogRow
|
||
|
||
if (!log.adminUserId) {
|
||
return <span className="text-text-muted text-xs">—</span>
|
||
}
|
||
|
||
return (
|
||
<AdminTableViewButton
|
||
label="مشاهده ادمین"
|
||
mode="navigate"
|
||
to={APP_ROUTES.USER_DETAIL(log.adminUserId)}
|
||
/>
|
||
)
|
||
},
|
||
}}
|
||
</PaginatedList>
|
||
</div>
|
||
|
||
<Modal
|
||
hideFooter
|
||
isOpen={isOpen}
|
||
size="lg"
|
||
title="پارامترهای درخواست"
|
||
onOpenChange={onOpenChange}
|
||
>
|
||
<pre
|
||
className="whitespace-pre-wrap break-all text-start font-mono text-xs leading-relaxed"
|
||
dir="ltr"
|
||
>
|
||
{JSON.stringify(activeParams, null, 2)}
|
||
</pre>
|
||
</Modal>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export default AuditLogsPage
|