Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
210 lines
6.0 KiB
TypeScript
210 lines
6.0 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 } from '@/constants/routes'
|
||
import { getSmsStatus, SMS_STATUS_FILTER_ITEMS } from '@/constants/status'
|
||
import { formatPersonName, coerceToString } from '@/helpers'
|
||
import { API_ROUTES } from '@/services/config'
|
||
import { formatIranianMobile, formatPersianDate, truncateValue } from '@/lib/formatters'
|
||
|
||
interface SmsMessageRow {
|
||
id: string
|
||
notificationId: string
|
||
userId: string
|
||
mobile: string
|
||
messageBody: string
|
||
status: string
|
||
provider?: string | null
|
||
providerRef?: string | null
|
||
errorMessage?: string | null
|
||
sentAt?: string | null
|
||
deliveredAt?: string | null
|
||
createdAt: string
|
||
updatedAt: string
|
||
recipient?: {
|
||
firstName: string | null
|
||
lastName: string | null
|
||
}
|
||
}
|
||
|
||
// Read-only troubleshooting list — GET /admin/sms-messages only supports
|
||
// filtering by status/userId and sorting by createdAt (see
|
||
// backend/src/modules/notifications/notifications.service.ts,
|
||
// ADMIN_SMS_FILTER_KEYS / ALLOWED_SORT_FIELDS). Columns that aren't
|
||
// whitelisted there (provider, sentAt, deliveredAt) are shown but not
|
||
// marked filterable/sortable, so the UI never implies a capability the
|
||
// API doesn't have.
|
||
const columns: PaginationListColumnType[] = [
|
||
{
|
||
field: 'userId',
|
||
label: 'گیرنده',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'text',
|
||
},
|
||
{
|
||
field: 'mobile',
|
||
label: 'موبایل',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'text',
|
||
},
|
||
{
|
||
field: 'messageBody',
|
||
label: 'متن پیامک',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'status',
|
||
label: 'وضعیت',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'select',
|
||
filterItems: SMS_STATUS_FILTER_ITEMS,
|
||
},
|
||
{
|
||
field: 'provider',
|
||
label: 'ارائهدهنده',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'providerRef',
|
||
label: 'کد پیگیری',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'sentAt',
|
||
label: 'زمان ارسال',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'deliveredAt',
|
||
label: 'زمان تحویل',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'createdAt',
|
||
label: 'تاریخ ثبت',
|
||
filterable: false,
|
||
sortable: true,
|
||
},
|
||
{
|
||
field: 'actions',
|
||
label: 'عملیات',
|
||
},
|
||
]
|
||
|
||
const SmsMessagesPanel = () => {
|
||
const { isOpen, onOpenChange, onOpen } = useDisclosure()
|
||
const [activeMessage, setActiveMessage] = useState<SmsMessageRow | null>(null)
|
||
|
||
const openMessageModal = (row: SmsMessageRow) => {
|
||
setActiveMessage(row)
|
||
onOpen()
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<PaginatedList
|
||
columns={columns}
|
||
url={API_ROUTES.SMS_MESSAGES.ADMIN_LIST}
|
||
>
|
||
{{
|
||
userId: (row) => {
|
||
const message = row as unknown as SmsMessageRow
|
||
const name = formatPersonName(message.recipient?.firstName, message.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(message.mobile)}
|
||
</span>
|
||
</div>
|
||
)
|
||
},
|
||
mobile: (_row, cellValue) => <span dir="ltr">{formatIranianMobile(coerceToString(cellValue))}</span>,
|
||
messageBody: (row, cellValue) => {
|
||
const message = row as unknown as SmsMessageRow
|
||
const body = coerceToString(cellValue)
|
||
|
||
return (
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-sm">{truncateValue(body)}</span>
|
||
{body.length > 80 && (
|
||
<Button
|
||
className="w-fit"
|
||
size="sm"
|
||
variant="light"
|
||
onClick={() => {
|
||
openMessageModal(message)
|
||
}}
|
||
>
|
||
نمایش کامل
|
||
</Button>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
status: (row, cellValue) => {
|
||
const message = row as unknown as SmsMessageRow
|
||
const { label, chipColor } = getSmsStatus(coerceToString(cellValue))
|
||
|
||
return (
|
||
<StatusChip
|
||
chipColor={chipColor}
|
||
description={message.status === 'failed' ? message.errorMessage : undefined}
|
||
label={label}
|
||
/>
|
||
)
|
||
},
|
||
provider: (_row, cellValue) => (cellValue ? coerceToString(cellValue) : '—'),
|
||
providerRef: (_row, cellValue) => (cellValue ? coerceToString(cellValue) : '—'),
|
||
sentAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
deliveredAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
actions: (row) => {
|
||
const message = row as unknown as SmsMessageRow
|
||
|
||
return (
|
||
<AdminTableViewButton
|
||
label="مشاهده کاربر"
|
||
mode="navigate"
|
||
to={APP_ROUTES.USER_DETAIL(message.userId)}
|
||
/>
|
||
)
|
||
},
|
||
}}
|
||
</PaginatedList>
|
||
|
||
<Modal
|
||
hideFooter
|
||
isOpen={isOpen}
|
||
size="lg"
|
||
title="متن کامل پیامک"
|
||
onOpenChange={onOpenChange}
|
||
>
|
||
<p className="whitespace-pre-wrap text-sm leading-relaxed">{activeMessage?.messageBody}</p>
|
||
</Modal>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default SmsMessagesPanel
|