Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
219 lines
7.3 KiB
TypeScript
219 lines
7.3 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 PushDeliveryRow {
|
||
id: string
|
||
notificationId: string
|
||
userId: string
|
||
subscriptionId: string
|
||
mobile: string
|
||
category: string
|
||
title: string
|
||
body: string
|
||
actionUrl: string | null
|
||
status: string
|
||
errorMessage: string | null
|
||
userAgent: string | null
|
||
displayedAt: string | null
|
||
openedAt: 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: 'status',
|
||
label: 'ارسال',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'select',
|
||
filterItems: [
|
||
{ code: 'sent', name: 'ارسالشده' },
|
||
{ code: 'failed', name: 'ناموفق' },
|
||
],
|
||
},
|
||
{ field: 'displayedAt', label: 'نمایش', filterable: false, sortable: false },
|
||
{ field: 'openedAt', label: 'بازشدن', filterable: false, sortable: false },
|
||
{ field: 'createdAt', label: 'تاریخ ثبت', filterable: false, sortable: true },
|
||
{ field: 'actions', label: 'عملیات' },
|
||
]
|
||
|
||
const getPushStatus = (row: PushDeliveryRow) => {
|
||
if (row.status === 'failed') {
|
||
return {
|
||
label: 'ناموفق',
|
||
chipColor: 'danger' as const,
|
||
description: row.errorMessage ?? undefined,
|
||
}
|
||
}
|
||
|
||
if (row.openedAt) {
|
||
return { label: 'باز شده', chipColor: 'success' as const }
|
||
}
|
||
|
||
if (row.displayedAt) {
|
||
return { label: 'نمایش داده شده', chipColor: 'warning' as const }
|
||
}
|
||
|
||
return { label: 'ارسالشده', chipColor: 'default' as const }
|
||
}
|
||
|
||
const PushDeliveriesPanel = () => {
|
||
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
||
const [activeDelivery, setActiveDelivery] = useState<PushDeliveryRow | null>(null)
|
||
|
||
const openDetails = (row: PushDeliveryRow) => {
|
||
setActiveDelivery(row)
|
||
onOpen()
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<PaginatedList
|
||
columns={columns}
|
||
url={API_ROUTES.PUSH_DELIVERIES.ADMIN_LIST}
|
||
>
|
||
{{
|
||
userId: (row) => {
|
||
const delivery = row as unknown as PushDeliveryRow
|
||
const name = formatPersonName(delivery.recipient?.firstName, delivery.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(delivery.mobile)}
|
||
</span>
|
||
</div>
|
||
)
|
||
},
|
||
mobile: (_row, cellValue) => <span dir="ltr">{formatIranianMobile(coerceToString(cellValue))}</span>,
|
||
title: (row, cellValue) => {
|
||
const delivery = row as unknown as PushDeliveryRow
|
||
|
||
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(delivery.body, 70)}</span>
|
||
</div>
|
||
)
|
||
},
|
||
status: (row) => {
|
||
const delivery = row as unknown as PushDeliveryRow
|
||
const status = getPushStatus(delivery)
|
||
|
||
return (
|
||
<StatusChip
|
||
chipColor={status.chipColor}
|
||
description={status.description}
|
||
label={status.label}
|
||
/>
|
||
)
|
||
},
|
||
displayedAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
openedAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
actions: (row) => {
|
||
const delivery = row as unknown as PushDeliveryRow
|
||
|
||
return (
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
size="sm"
|
||
variant="light"
|
||
onClick={() => {
|
||
openDetails(delivery)
|
||
}}
|
||
>
|
||
جزئیات
|
||
</Button>
|
||
<AdminTableViewButton
|
||
label="مشاهده کاربر"
|
||
mode="navigate"
|
||
to={APP_ROUTES.USER_DETAIL(delivery.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>{activeDelivery?.title ?? '—'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">متن اعلان</p>
|
||
<p className="whitespace-pre-wrap">{activeDelivery?.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">{activeDelivery?.actionUrl ?? CONSUMER_ROUTES.PROFILE_NOTIFICATIONS}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">دسته</p>
|
||
<p>{activeDelivery?.category ?? '—'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">زمان نمایش</p>
|
||
<p>{formatPersianDate(activeDelivery?.displayedAt)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">زمان بازشدن</p>
|
||
<p>{formatPersianDate(activeDelivery?.openedAt)}</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-secondary-10">User-Agent</p>
|
||
<p
|
||
className="break-all text-xs text-secondary-30"
|
||
dir="ltr"
|
||
>
|
||
{activeDelivery?.userAgent ?? '—'}
|
||
</p>
|
||
</div>
|
||
{activeDelivery?.errorMessage ? (
|
||
<div>
|
||
<p className="font-semibold text-fourth-900">خطای تحویل</p>
|
||
<p className="whitespace-pre-wrap text-fourth-900">{activeDelivery.errorMessage}</p>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</Modal>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default PushDeliveriesPanel
|