admin/features/events/detail/admin-event-detail/AdminEventBookingsTab.tsx
alisaza 0a68104e85 chore(openapi): format tags in API documentation for consistency
Updated the OpenAPI specification to format the 'tags' property as an array on multiple endpoints, ensuring consistent styling across the documentation. Additionally, simplified JSX return statements in the BookingsPage and AdminEventBookingsTab components for improved readability.
2026-09-11 23:00:12 +03:30

164 lines
6.3 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 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: 'cancellationReason', label: 'دلیل لغو', filterable: false, sortable: false, type: 'text' },
{ 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}
/>
)
},
cancellationReason: (row, cellValue) => {
const reason = coerceToString(cellValue).trim()
const isCancelledLike = row.status === 'cancelled' || row.status === 'refunded'
if (!isCancelledLike || !reason) return '—'
return <span className="max-w-[220px] whitespace-pre-wrap break-words text-xs text-secondary-20">{reason}</span>
},
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