Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
244 lines
8.8 KiB
TypeScript
244 lines
8.8 KiB
TypeScript
import type { PaginationListColumnType } from '@/types'
|
||
import type { AdminUserDetail } from '@/services/adminUserDetail'
|
||
import { Card, CardBody, CardHeader } from '@/components/heroui/Card'
|
||
import { Chip } from '@/components/heroui/Chip'
|
||
import StatusChip from '@/components/ui/StatusChip'
|
||
import { getBooleanStatus, getIdentityStatus, getUserAccountStatus, getUserRole } from '@/constants/status'
|
||
import { formatNumber } from '@/helpers'
|
||
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
|
||
|
||
export const formatToman = (value: unknown) => {
|
||
const amount = Number(value)
|
||
|
||
return Number.isFinite(amount) ? `${formatNumber(amount)} تومان` : '—'
|
||
}
|
||
|
||
const column = (field: string, label: string, type?: 'text' | 'date'): PaginationListColumnType => ({
|
||
field,
|
||
label,
|
||
filterable: false,
|
||
sortable: false,
|
||
...(type ? { type } : {}),
|
||
})
|
||
|
||
export const hostedEventsColumns = [
|
||
column('title', 'عنوان', 'text'),
|
||
column('status', 'وضعیت'),
|
||
column('startsAt', 'شروع', 'date'),
|
||
column('bookedCount', 'ظرفیت پرشده'),
|
||
column('capacity', 'ظرفیت کل'),
|
||
column('isFree', 'رایگان'),
|
||
column('price', 'قیمت'),
|
||
column('createdAt', 'تاریخ ثبت', 'date'),
|
||
]
|
||
export const bookingsColumns = [
|
||
column('bookingCode', 'کد رزرو', 'text'),
|
||
column('eventTitle', 'رویداد', 'text'),
|
||
column('status', 'وضعیت'),
|
||
column('checkedInAt', 'چکاین', 'date'),
|
||
column('createdAt', 'تاریخ ثبت', 'date'),
|
||
]
|
||
export const paymentsColumns = [
|
||
column('paymentCode', 'کد پرداخت', 'text'),
|
||
column('eventTitle', 'رویداد', 'text'),
|
||
column('method', 'روش'),
|
||
column('status', 'وضعیت'),
|
||
column('totalAmount', 'مبلغ کل'),
|
||
column('createdAt', 'تاریخ ثبت', 'date'),
|
||
]
|
||
export const reviewsColumns = [
|
||
column('eventTitle', 'رویداد', 'text'),
|
||
column('authorName', 'نویسنده', 'text'),
|
||
column('rating', 'امتیاز'),
|
||
column('body', 'متن', 'text'),
|
||
column('status', 'وضعیت'),
|
||
column('createdAt', 'تاریخ ثبت', 'date'),
|
||
]
|
||
export const followsColumns = [
|
||
column('mobile', 'موبایل', 'text'),
|
||
column('name', 'نام', 'text'),
|
||
column('notifyNewEvents', 'اعلان رویداد جدید'),
|
||
column('createdAt', 'تاریخ', 'date'),
|
||
]
|
||
export const reportsColumns = [
|
||
column('otherUserMobile', 'موبایل طرف مقابل', 'text'),
|
||
column('otherUserName', 'نام طرف مقابل', 'text'),
|
||
column('reasonLabel', 'دلیل', 'text'),
|
||
column('status', 'وضعیت'),
|
||
column('createdAt', 'تاریخ', 'date'),
|
||
]
|
||
export const blocksColumns = [
|
||
column('otherUserMobile', 'موبایل', 'text'),
|
||
column('otherUserName', 'نام', 'text'),
|
||
column('createdAt', 'تاریخ', 'date'),
|
||
]
|
||
|
||
const DetailItem = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||
<div className="flex min-h-20 flex-col justify-center gap-2 rounded-xl border border-secondary-40 bg-secondary-50/60 p-3 text-right">
|
||
<span className="text-2xs text-secondary-30">{label}</span>
|
||
<span className="font-semibold text-secondary-20">{children}</span>
|
||
</div>
|
||
)
|
||
|
||
/** Compact profile strip — name is already in the page navbar. */
|
||
export const AdminUserOverview = ({ detail }: { detail: AdminUserDetail }) => {
|
||
const role = getUserRole(detail.role)
|
||
const accountStatus = getUserAccountStatus(detail.status)
|
||
const identityStatus = getIdentityStatus(detail.identityStatus)
|
||
|
||
return (
|
||
<Card className="admin-surface">
|
||
<CardBody className="flex flex-row flex-wrap items-center gap-2 p-3">
|
||
<Chip
|
||
dir="ltr"
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
{formatIranianMobile(detail.mobile)}
|
||
</Chip>
|
||
{detail.cityName ? (
|
||
<Chip
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
{detail.cityName}
|
||
</Chip>
|
||
) : null}
|
||
<Chip
|
||
color={role.chipColor}
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
{role.label}
|
||
</Chip>
|
||
<Chip
|
||
color={accountStatus.chipColor}
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
{accountStatus.label}
|
||
</Chip>
|
||
<Chip
|
||
color={identityStatus.chipColor}
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
احراز · {identityStatus.label}
|
||
</Chip>
|
||
<Chip
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
کیفپول · {formatToman(detail.walletBalance)}
|
||
</Chip>
|
||
<Chip
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
آخرین ورود · {formatPersianDate(detail.lastLoginAt)}
|
||
</Chip>
|
||
<Chip
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
ثبتنام · {formatPersianDate(detail.createdAt)}
|
||
</Chip>
|
||
{detail.dateOfBirth ? (
|
||
<Chip
|
||
size="sm"
|
||
variant="flat"
|
||
>
|
||
تولد · {formatPersianDate(detail.dateOfBirth, { dateStyle: 'medium' })}
|
||
</Chip>
|
||
) : null}
|
||
</CardBody>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
export const AdminUserContactInfo = ({ detail }: { detail: AdminUserDetail }) => {
|
||
const links = detail.contactLinks ?? []
|
||
const hasAddress = Boolean(detail.defaultAddress?.trim())
|
||
const hasLinks = links.length > 0
|
||
|
||
if (!hasAddress && !hasLinks && !detail.bio) {
|
||
return (
|
||
<Card className="admin-surface">
|
||
<CardHeader className="border-b border-secondary-40 px-5 py-4 text-sm font-bold text-secondary-10">اطلاعات تماس</CardHeader>
|
||
<CardBody className="p-5 text-sm text-secondary-30">هنوز آدرس ثابت یا لینک تماسی ثبت نشده است.</CardBody>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Card className="admin-surface">
|
||
<CardHeader className="border-b border-secondary-40 px-5 py-4 text-sm font-bold text-secondary-10">اطلاعات تماس</CardHeader>
|
||
<CardBody className="flex flex-col gap-4 p-4">
|
||
{detail.bio ? <DetailItem label="بیوگرافی">{detail.bio}</DetailItem> : null}
|
||
{hasAddress ? <DetailItem label="آدرس ثابت میزبان">{detail.defaultAddress}</DetailItem> : null}
|
||
{hasLinks ? (
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-2xs text-secondary-30">راههای تماس</span>
|
||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||
{links.map((link) => (
|
||
<div
|
||
key={link.id}
|
||
className="rounded-xl border border-secondary-40 bg-secondary-50/60 p-3 text-right"
|
||
>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className="text-xs font-bold text-primary">{link.label}</span>
|
||
<StatusChip {...getBooleanStatus(link.isPublic, { true: 'عمومی', false: 'فقط شرکتکنندگان' })} />
|
||
</div>
|
||
<a
|
||
className="mt-2 block break-all text-sm font-semibold text-secondary-20 hover:text-primary"
|
||
dir="ltr"
|
||
href={link.url}
|
||
rel="noreferrer"
|
||
target="_blank"
|
||
>
|
||
{link.value}
|
||
</a>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</CardBody>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
export const AdminUserActivitySummary = ({ detail }: { detail: AdminUserDetail }) => {
|
||
const summary = [
|
||
['رویدادهای میزبانیشده', detail.summary.hostedEventsCount],
|
||
['رزروها', detail.summary.bookingsCount],
|
||
['پرداختها', detail.summary.paymentsCount],
|
||
['نظرات نوشتهشده', detail.summary.reviewsWrittenCount],
|
||
['نظرات دریافتشده', detail.summary.reviewsReceivedCount],
|
||
['دنبالشوندهها', detail.summary.followingCount],
|
||
['دنبالکنندهها', detail.summary.followersCount],
|
||
['گزارشهای ثبتشده', detail.summary.reportsFiledCount],
|
||
['گزارشهای دریافتشده', detail.summary.reportsReceivedCount],
|
||
['بلاککرده', detail.summary.blockingCount],
|
||
['بلاکشده توسط', detail.summary.blockedByCount],
|
||
] as const
|
||
|
||
return (
|
||
<Card className="admin-surface">
|
||
<CardHeader className="border-b border-secondary-40 px-5 py-4 text-sm font-bold text-secondary-10">خلاصه فعالیتها</CardHeader>
|
||
<CardBody className="p-4">
|
||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||
{summary.map(([label, value]) => (
|
||
<DetailItem
|
||
key={label}
|
||
label={label}
|
||
>
|
||
<span className="text-xl font-bold text-secondary-10">{value}</span>
|
||
</DetailItem>
|
||
))}
|
||
</div>
|
||
</CardBody>
|
||
</Card>
|
||
)
|
||
}
|