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.
171 lines
6.5 KiB
TypeScript
171 lines
6.5 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'
|
||
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
|