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

210 lines
6.0 KiB
TypeScript
Raw 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 } 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