Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
155 lines
5.8 KiB
TypeScript
155 lines
5.8 KiB
TypeScript
'use client'
|
||
|
||
import type { BookingUser } from './types'
|
||
|
||
import { useRef } from 'react'
|
||
|
||
import type { PaginationListColumnType } from '@/types'
|
||
import type { PaginatedListHandle } from '@/components/PaginatedList'
|
||
import Button from '@/components/formElements/Button'
|
||
import FileCheckIcon from '@/components/icons/FileCheckIcon'
|
||
import PaginatedList from '@/components/PaginatedList'
|
||
import AdminTableActions from '@/components/ui/AdminTableActions'
|
||
import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
|
||
import StatusChip from '@/components/ui/StatusChip'
|
||
import { APP_ROUTES } from '@/constants/routes'
|
||
import { BOOKING_STATUS_FILTER_ITEMS, getBookingStatus } from '@/constants/status'
|
||
import { formatPersonName, coerceToString } from '@/helpers'
|
||
import { checkInBookingAsAdmin } from '@/services/eventManagement'
|
||
import { API_ROUTES } from '@/services/config'
|
||
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
|
||
import useAdminAction from '@/hooks/useAdminAction'
|
||
|
||
// Bookings tab columns — copied from app/(dashboard)/bookings/page.tsx,
|
||
// minus the `eventId` column (this list is already scoped to one event via
|
||
// urlParams.filters). Cancel/refund stay on their existing flows — an
|
||
// organizer cannot cancel a single booking either (business-rules.md), only
|
||
// check-in is exposed here, mirroring the host's own attendance action.
|
||
// `search` is a filter-only synthetic column (hideInTable) — its value
|
||
// round-trips as filters.search, matched server-side (adminList in
|
||
// bookings.service.ts) against booking code and guest first/last name, the
|
||
// same lookup a host's attendance search covers.
|
||
const bookingsColumns: PaginationListColumnType[] = [
|
||
{ field: 'search', label: 'جستجو (کد رزرو یا نام مهمان)', filterable: true, sortable: false, type: 'text', hideInTable: true },
|
||
{ field: 'bookingCode', label: 'کد رزرو', filterable: false, sortable: false, type: 'text' },
|
||
{ field: 'userId', label: 'مهمان', filterable: false, sortable: false, type: 'text' },
|
||
{ field: 'status', label: 'وضعیت', filterable: true, sortable: true, type: 'select', filterItems: BOOKING_STATUS_FILTER_ITEMS },
|
||
{ field: 'checkedInAt', label: 'چکاین', filterable: false, sortable: false, type: 'date' },
|
||
{ field: 'createdAt', label: 'تاریخ ثبت', filterable: false, sortable: true, type: 'date' },
|
||
{ field: 'actions', label: 'عملیات' },
|
||
]
|
||
|
||
interface AdminEventBookingsTabProps {
|
||
eventId: string
|
||
eventStatus: string
|
||
onCheckedIn?: () => void
|
||
}
|
||
|
||
const AdminEventBookingsTab = ({ eventId, eventStatus, onCheckedIn }: AdminEventBookingsTabProps) => {
|
||
const bookingsListRef = useRef<PaginatedListHandle>(null)
|
||
const { pendingId, runAction } = useAdminAction()
|
||
|
||
const handleCheckIn = (bookingId: string) =>
|
||
runAction(
|
||
bookingId,
|
||
async () => {
|
||
await checkInBookingAsAdmin(bookingId)
|
||
bookingsListRef.current?.refresh()
|
||
onCheckedIn?.()
|
||
},
|
||
'حضور مهمان ثبت شد'
|
||
)
|
||
|
||
return (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="flex justify-end">
|
||
<Button
|
||
size="sm"
|
||
to={`${APP_ROUTES.BOOKINGS}?filters[eventId]=${eventId}`}
|
||
variant="flat"
|
||
>
|
||
مشاهده در لیست کلی
|
||
</Button>
|
||
</div>
|
||
<PaginatedList
|
||
ref={bookingsListRef}
|
||
columns={bookingsColumns}
|
||
tableWrapperClass="max-h-[360px] p-0"
|
||
url={API_ROUTES.BOOKINGS.ADMIN_LIST}
|
||
urlParams={{
|
||
page: 1,
|
||
pageSize: 10,
|
||
sort: '',
|
||
filters: { eventId },
|
||
}}
|
||
>
|
||
{{
|
||
userId: (row) => {
|
||
const user = row.user as BookingUser | undefined
|
||
|
||
if (!user) return '—'
|
||
|
||
const name = formatPersonName(user.firstName ?? undefined, user.lastName ?? undefined)
|
||
|
||
return (
|
||
<div className="flex flex-col gap-1">
|
||
<span>{name}</span>
|
||
<span
|
||
className="text-xs text-tertiary-300"
|
||
dir="ltr"
|
||
>
|
||
{formatIranianMobile(user.mobile)}
|
||
</span>
|
||
</div>
|
||
)
|
||
},
|
||
status: (_row, cellValue) => {
|
||
const { label, chipColor } = getBookingStatus(coerceToString(cellValue))
|
||
|
||
return (
|
||
<StatusChip
|
||
chipColor={chipColor}
|
||
label={label}
|
||
/>
|
||
)
|
||
},
|
||
checkedInAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
createdAt: (_row, cellValue) => formatPersianDate(cellValue),
|
||
actions: (row) => {
|
||
const user = row.user as BookingUser | undefined
|
||
const bookingId = row.id as string
|
||
const canCheckIn = row.status === 'confirmed' && !row.checkedInAt && ['published', 'full'].includes(eventStatus)
|
||
|
||
return (
|
||
<AdminTableActions>
|
||
{user ? (
|
||
<AdminTableViewButton
|
||
label="مشاهده کاربر"
|
||
mode="navigate"
|
||
to={APP_ROUTES.USER_DETAIL(user.id)}
|
||
/>
|
||
) : null}
|
||
{canCheckIn ? (
|
||
<Button
|
||
iconOnly
|
||
aria-label="ثبت حضور"
|
||
color="success"
|
||
isLoading={pendingId === bookingId}
|
||
size="sm"
|
||
variant="flat"
|
||
onClick={() => void handleCheckIn(bookingId)}
|
||
>
|
||
<FileCheckIcon className="size-4" />
|
||
</Button>
|
||
) : null}
|
||
</AdminTableActions>
|
||
)
|
||
},
|
||
}}
|
||
</PaginatedList>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AdminEventBookingsTab
|