admin/app/(dashboard)/notifications/_components/InAppNotificationsPanel.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

180 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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