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.
164 lines
6.3 KiB
TypeScript
164 lines
6.3 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: '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
|