Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
205 lines
6.2 KiB
TypeScript
205 lines
6.2 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
|
||
import type { PaginationListColumnType } from '@/types'
|
||
import PaginatedList from '@/components/PaginatedList'
|
||
import PageNavbar from '@/components/layouts/PageNavbar'
|
||
import Button from '@/components/formElements/Button'
|
||
import FileCheckIcon from '@/components/icons/FileCheckIcon'
|
||
import Modal from '@/components/modals/Modal'
|
||
import AdminTableActions from '@/components/ui/AdminTableActions'
|
||
import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
|
||
import StatusChip from '@/components/ui/StatusChip'
|
||
import axiosInstance from '@/config/axios'
|
||
import useAdminMutation from '@/hooks/useAdminMutation'
|
||
import { coerceToString } from '@/helpers'
|
||
import { formatIranianMobile, formatPersianDate, truncateValue } from '@/lib/formatters'
|
||
import { getBooleanStatus } from '@/constants/status'
|
||
import { API_ROUTES } from '@/services/config'
|
||
|
||
interface ContactMessageRow {
|
||
id: string
|
||
fullName: string
|
||
mobile: string
|
||
subject: string
|
||
message: string
|
||
readAt?: string | null
|
||
readBy?: string | null
|
||
createdAt: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
const columns: PaginationListColumnType[] = [
|
||
{
|
||
field: 'fullName',
|
||
label: 'نام',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'mobile',
|
||
label: 'شماره تماس',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'subject',
|
||
label: 'موضوع',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'message',
|
||
label: 'پیام',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
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 ContactMessagesPage = () => {
|
||
const { pendingId, runAction } = useAdminMutation({ url: API_ROUTES.CONTACT_MESSAGES.ADMIN_LIST })
|
||
const [selected, setSelected] = useState<ContactMessageRow | null>(null)
|
||
|
||
return (
|
||
<section className="h-full w-full text-right">
|
||
<PageNavbar pageTitle="پیامهای تماس" />
|
||
<div className="admin-page-container">
|
||
<PaginatedList
|
||
columns={columns}
|
||
url={API_ROUTES.CONTACT_MESSAGES.ADMIN_LIST}
|
||
>
|
||
{{
|
||
fullName: (_row, cellValue) => coerceToString(cellValue),
|
||
mobile: (_row, cellValue) => <span dir="ltr">{formatIranianMobile(coerceToString(cellValue))}</span>,
|
||
subject: (_row, cellValue) => coerceToString(cellValue),
|
||
message: (row, cellValue) => {
|
||
const message = row as ContactMessageRow
|
||
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={() => {
|
||
setSelected(message)
|
||
}}
|
||
>
|
||
نمایش کامل
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
)
|
||
},
|
||
readStatus: (row) => {
|
||
const message = row as ContactMessageRow
|
||
const { label, chipColor } = getBooleanStatus(Boolean(message.readAt), {
|
||
true: 'خواندهشده',
|
||
false: 'خواندهنشده',
|
||
})
|
||
|
||
return (
|
||
<StatusChip
|
||
chipColor={chipColor}
|
||
label={label}
|
||
/>
|
||
)
|
||
},
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
actions: (row) => {
|
||
const message = row as ContactMessageRow
|
||
|
||
return (
|
||
<AdminTableActions>
|
||
<AdminTableViewButton
|
||
label="مشاهده پیام"
|
||
mode="open-detail"
|
||
onClick={() => {
|
||
setSelected(message)
|
||
}}
|
||
/>
|
||
{message.readAt ? null : (
|
||
<Button
|
||
iconOnly
|
||
aria-label="علامتگذاری بهعنوان خواندهشده"
|
||
isLoading={pendingId === message.id}
|
||
size="sm"
|
||
variant="light"
|
||
onClick={() =>
|
||
void runAction(
|
||
message.id,
|
||
() => axiosInstance.patch(API_ROUTES.CONTACT_MESSAGES.ADMIN_READ(message.id)),
|
||
'پیام خوانده شد'
|
||
)
|
||
}
|
||
>
|
||
<FileCheckIcon className="size-5" />
|
||
</Button>
|
||
)}
|
||
</AdminTableActions>
|
||
)
|
||
},
|
||
}}
|
||
</PaginatedList>
|
||
</div>
|
||
|
||
<Modal
|
||
hideFooter
|
||
isOpen={Boolean(selected)}
|
||
size="lg"
|
||
title={selected?.subject ?? 'پیام تماس'}
|
||
onOpenChange={(open) => {
|
||
if (!open) setSelected(null)
|
||
}}
|
||
>
|
||
{selected ? (
|
||
<div className="space-y-4 text-sm">
|
||
<div>
|
||
<p className="font-bold">فرستنده</p>
|
||
<p className="mt-1">{selected.fullName}</p>
|
||
<p
|
||
className="mt-1 text-text-muted"
|
||
dir="ltr"
|
||
>
|
||
{formatIranianMobile(selected.mobile)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="font-bold">پیام</p>
|
||
<p className="mt-1 whitespace-pre-wrap leading-relaxed">{selected.message}</p>
|
||
</div>
|
||
<p className="text-text-muted">{formatPersianDate(selected.createdAt)}</p>
|
||
</div>
|
||
) : null}
|
||
</Modal>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export default ContactMessagesPage
|