Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
|
||
import type { PaginationListColumnType } from '@/types'
|
||
import useDisclosure from '@/hooks/useDisclosure'
|
||
import PaginatedList from '@/components/PaginatedList'
|
||
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, CONSUMER_ROUTES } from '@/constants/routes'
|
||
import { coerceToString, formatPersonName } from '@/helpers'
|
||
import { API_ROUTES } from '@/services/config'
|
||
import { formatIranianMobile, formatPersianDate, truncateValue } from '@/lib/formatters'
|
||
|
||
interface InAppNotificationRow {
|
||
id: string
|
||
userId: string
|
||
mobile: string
|
||
category: string
|
||
title: string
|
||
body: string
|
||
actionUrl: string | null
|
||
status: string
|
||
readAt: string | null
|
||
createdAt: string
|
||
recipient?: {
|
||
firstName: string | null
|
||
lastName: string | null
|
||
}
|
||
}
|
||
|
||
const columns: PaginationListColumnType[] = [
|
||
{ field: 'userId', label: 'گیرنده', filterable: true, sortable: false, type: 'text' },
|
||
{ field: 'mobile', label: 'موبایل', filterable: true, sortable: false, type: 'text' },
|
||
{ field: 'title', label: 'عنوان', filterable: false, sortable: false },
|
||
{ field: 'category', label: 'دسته', filterable: true, sortable: false, type: 'text' },
|
||
{
|
||
field: 'readStatus',
|
||
label: 'خواندن',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'select',
|
||
filterItems: [
|
||
{ code: 'unread', name: 'خواندهنشده' },
|
||
{ code: 'read', name: 'خواندهشده' },
|
||
],
|
||
},
|
||
{ field: 'createdAt', label: 'تاریخ ثبت', filterable: false, sortable: true },
|
||
{ field: 'actions', label: 'عملیات' },
|
||
]
|
||
|
||
const InAppNotificationsPanel = () => {
|
||
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
||
const [activeRow, setActiveRow] = useState<InAppNotificationRow | null>(null)
|
||
|
||
const openDetails = (row: InAppNotificationRow) => {
|
||
setActiveRow(row)
|
||
onOpen()
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<PaginatedList
|
||
columns={columns}
|
||
url={API_ROUTES.IN_APP_NOTIFICATIONS.ADMIN_LIST}
|
||
>
|
||
{{
|
||
userId: (row) => {
|
||
const notification = row as unknown as InAppNotificationRow
|
||
const name = formatPersonName(notification.recipient?.firstName, notification.recipient?.lastName)
|
||
|
||
return (
|
||
<div className="flex flex-col gap-1">
|
||
{name !== '—' && <span className="text-sm">{name}</span>}
|
||
<span
|
||
className="text-text-muted text-xs"
|
||
dir="ltr"
|
||
>
|
||
{formatIranianMobile(notification.mobile)}
|
||
</span>
|
||
</div>
|
||
)
|
||
},
|
||
mobile: (_row, cellValue) => <span dir="ltr">{formatIranianMobile(coerceToString(cellValue))}</span>,
|
||
title: (row, cellValue) => {
|
||
const notification = row as unknown as InAppNotificationRow
|
||
|
||
return (
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-sm">{truncateValue(coerceToString(cellValue), 50)}</span>
|
||
<span className="text-text-muted text-xs">{truncateValue(notification.body, 70)}</span>
|
||
</div>
|
||
)
|
||
},
|
||
category: (_row, cellValue) => coerceToString(cellValue) || '—',
|
||
readStatus: (row) => {
|
||
const notification = row as unknown as InAppNotificationRow
|
||
|
||
return notification.readAt ? (
|
||
<StatusChip
|
||
chipColor="success"
|
||
label="خواندهشده"
|
||
/>
|
||
) : (
|
||
<StatusChip
|
||
chipColor="warning"
|
||
label="خواندهنشده"
|
||
/>
|
||
)
|
||
},
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
actions: (row) => {
|
||
const notification = row as unknown as InAppNotificationRow
|
||
|
||
return (
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
size="sm"
|
||
variant="light"
|
||
onClick={() => {
|
||
openDetails(notification)
|
||
}}
|
||
>
|
||
جزئیات
|
||
</Button>
|
||
<AdminTableViewButton
|
||
label="مشاهده کاربر"
|
||
mode="navigate"
|
||
to={APP_ROUTES.USER_DETAIL(notification.userId)}
|
||
/>
|
||
</div>
|
||
)
|
||
},
|
||
}}
|
||
</PaginatedList>
|
||
|
||
<Modal
|
||
hideFooter
|
||
isOpen={isOpen}
|
||
size="2xl"
|
||
title="جزئیات اعلان درونبرنامهای"
|
||
onOpenChange={onOpenChange}
|
||
>
|
||
<div className="space-y-4 text-sm leading-7">
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">عنوان</p>
|
||
<p>{activeRow?.title ?? '—'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">متن اعلان</p>
|
||
<p className="whitespace-pre-wrap">{activeRow?.body ?? '—'}</p>
|
||
</div>
|
||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">مسیر مقصد</p>
|
||
<p dir="ltr">{activeRow?.actionUrl ?? CONSUMER_ROUTES.PROFILE_NOTIFICATIONS}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">دسته</p>
|
||
<p>{activeRow?.category ?? '—'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">وضعیت خواندن</p>
|
||
<p>{activeRow?.readAt ? formatPersianDate(activeRow.readAt) : 'خوانده نشده'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">تاریخ ثبت</p>
|
||
<p>{formatPersianDate(activeRow?.createdAt)}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default InAppNotificationsPanel
|