admin/features/events/detail/admin-event-detail/AdminEventBookingsTab.tsx
alisaza 3b86c2c72f feat(api): add notifications endpoints for user notifications
Introduced new API endpoints for managing in-app notifications. The `/api/v1/notifications/me` endpoint retrieves a paginated list of notifications for the authenticated user, while the `/api/v1/notifications/me/unread-count` endpoint returns the count of unread notifications. Enhanced the OpenAPI documentation to reflect these changes, including detailed parameter descriptions and response schemas for better clarity and usability.
2026-09-13 13:04:02 +03:30

171 lines
6.5 KiB
TypeScript
Raw Permalink 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'
import useAlertModal from '@/hooks/useAlertModal'
// 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 { showAlert } = useAlertModal()
const handleCheckIn = (bookingId: string) => {
showAlert('حضور این مهمان ثبت شود؟', () =>
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={() => {
handleCheckIn(bookingId)
}}
>
<FileCheckIcon className="size-4" />
</Button>
) : null}
</AdminTableActions>
)
},
}}
</PaginatedList>
</div>
)
}
export default AdminEventBookingsTab