admin/features/admin-users/adminUserDetailUi.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

244 lines
8.8 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

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>
)
}