'use client'; import { useState } from 'react'; import { useLocale, useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import { Badge, Skeleton, Stack, Tab, Tabs, Typography } from '@mui/material'; import { AccentCard, AppButton, CountdownTimer, EmptyState, ErrorState, Money, RatingInput, StatusChip } from '@/components'; import type { AccentTone, StatusKind } from '@/components'; import { BOOKING_STATUS_KIND } from '@/components/booking/statusKind'; import { bookingReviewPath, ROUTES } from '@/constants'; import { formatShamsiDate, localeTag } from '@/utils'; import { useBookingList } from '@/services/bookings'; import { BOOKINGS_PAGE_SIZE } from '@/services/bookings/constants'; import type { BookingListItemDto, BookingStatus } from '@/services/bookings/types'; import { useCustomerRequests } from '@/services/bookingRequests'; import type { BookingRequestListItem, BookingRequestStatus } from '@/services/bookingRequests/types'; import { useReviewEligibility } from '@/services/reviews'; type BookingsTab = 'pending' | 'active' | 'past'; /** `pending_payment`/`confirmed`/`in_progress` are still unfolding; the rest are resolved. */ const ACTIVE_BOOKING_STATUSES: readonly BookingStatus[] = ['pending_payment', 'confirmed', 'in_progress']; const PAST_BOOKING_STATUSES: readonly BookingStatus[] = ['completed', 'disputed', 'closed', 'cancelled']; const PENDING_REQUEST_STATUSES: readonly BookingRequestStatus[] = [ 'pending_nurse_response', 'accepted_awaiting_payment', ]; const KIND_TO_ACCENT: Record = { neutral: 'neutral', info: 'info', pending: 'primary', verified: 'success', active: 'success', rejected: 'error', }; /** * Customer رزروها — the lifecycle home. Three tabs so a money-adjacent pending request is never orphaned * once the user leaves C5: **در انتظار پاسخ** wires the exported-but-previously-unused * `useCustomerRequests` (live mini-countdown per row, deep-linking back to C5); **فعال** / **گذشته** split * `useBookingList('customer')` by status. Rows carry a soft status chip + a matching `borderInlineStart` * accent and are fully tappable (keyboard-focusable). Pagination is a "load more" over a single growing * `pageSize` (the C2 results pattern) — booking #21+ stays reachable. */ export default function BookingsScreen() { const t = useTranslations('booking'); const router = useRouter(); const locale = useLocale(); const [tab, setTab] = useState('active'); const [pageSize, setPageSize] = useState(BOOKINGS_PAGE_SIZE); const pendingQuery = useCustomerRequests(); const pendingItems = (pendingQuery.data?.items ?? []).filter((item) => PENDING_REQUEST_STATUSES.includes(item.status), ); const bookingsQuery = useBookingList('customer', { page: 1, pageSize }); const allBookings = bookingsQuery.data?.items ?? []; const total = bookingsQuery.data?.total ?? 0; const hasMore = allBookings.length < total; const activeItems = allBookings.filter((item) => ACTIVE_BOOKING_STATUSES.includes(item.status)); const pastItems = allBookings.filter((item) => PAST_BOOKING_STATUSES.includes(item.status)); const openBooking = (id: number) => router.push(`/${locale}${ROUTES.BOOKINGS}/${id}`); const openRequest = (id: number) => router.push(`/${locale}${ROUTES.BOOKING_REQUEST_STATUS}/${id}`); const goToSearch = () => router.push(`/${locale}${ROUTES.SEARCH}`); return ( {t('list_title')} {t('list_subtitle')} setTab(value)} variant="fullWidth"> 0 ? ( {t('tab_pending')} ) : ( t('tab_pending') ) } /> {tab === 'pending' ? ( pendingQuery.isLoading ? ( ) : pendingQuery.isError ? ( pendingQuery.refetch()} /> ) : pendingItems.length === 0 ? ( {t('missing_nurse_cta')} } /> ) : ( {pendingItems.map((item) => ( openRequest(item.id)} /> ))} ) ) : null} {tab === 'active' ? ( bookingsQuery.isLoading ? ( ) : bookingsQuery.isError ? ( bookingsQuery.refetch()} /> ) : activeItems.length === 0 ? ( {t('missing_nurse_cta')} } /> ) : ( setPageSize((size) => size + BOOKINGS_PAGE_SIZE)} loadingMore={bookingsQuery.isFetching} loadMoreLabel={t('load_more')} /> ) ) : null} {tab === 'past' ? ( bookingsQuery.isLoading ? ( ) : bookingsQuery.isError ? ( bookingsQuery.refetch()} /> ) : pastItems.length === 0 ? ( ) : ( setPageSize((size) => size + BOOKINGS_PAGE_SIZE)} loadingMore={bookingsQuery.isFetching} loadMoreLabel={t('load_more')} /> ) ) : null} ); } function BookingRows({ items, locale, onOpen, hasMore, onLoadMore, loadingMore, loadMoreLabel, }: { items: BookingListItemDto[]; locale: string; onOpen: (id: number) => void; hasMore: boolean; onLoadMore: () => void; loadingMore: boolean; loadMoreLabel: string; }) { return ( {items.map((item) => ( onOpen(item.id)} /> ))} {hasMore ? ( {loadMoreLabel} ) : null} ); } function BookingRow({ item, locale, onOpen }: { item: BookingListItemDto; locale: string; onOpen: () => void }) { const t = useTranslations('booking'); const kind = BOOKING_STATUS_KIND[item.status]; const isCompleted = item.status === 'completed' || item.status === 'closed'; return ( { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onOpen(); } }} data-booking-row={item.id} sx={{ cursor: 'pointer', '&:focus-visible': { outline: '2px solid var(--bal-primary)', outlineOffset: 2 } }} > {item.counterpartyName} {formatShamsiDate(item.scheduledDate, locale)} · {t('session_count', { count: item.sessionCount })} {t('list_total')}: {isCompleted ? : null} ); } /** A completed booking without a review gets a compact star-strip CTA deep-linking into the review page. * The eligibility read is gated to completed/closed rows only (`enabled`) — an active booking never fires * it, and `canReview: false` (already reviewed or otherwise ineligible) renders nothing extra. */ function CompletedReviewStrip({ bookingId, enabled }: { bookingId: number; enabled: boolean }) { const t = useTranslations('reviews'); const router = useRouter(); const locale = useLocale(); const eligibility = useReviewEligibility(bookingId, { enabled }); if (!eligibility.data?.canReview) return null; return ( } onClick={(event) => { event.stopPropagation(); router.push(`/${locale}${bookingReviewPath(bookingId)}`); }} sx={{ alignSelf: 'flex-start', px: 0 }} > {t('cta_leave')} ); } /** «در انتظار پاسخ» row — a pending or accepted-awaiting-payment request, with a live mini-countdown. */ function PendingRequestRow({ item, locale, onOpen, }: { item: BookingRequestListItem; locale: string; onOpen: () => void; }) { const t = useTranslations('booking'); const accepted = item.status === 'accepted_awaiting_payment'; const deadline = accepted ? item.paymentDeadlineAt : item.nurseResponseDeadlineAt; const timeFmt = new Intl.DateTimeFormat(localeTag(locale), { hour: '2-digit', minute: '2-digit' }); const startDate = new Date(`${item.requestedDate}T${item.requestedTimeStart}`); const dateLabel = formatShamsiDate(startDate, locale); const timeLabel = timeFmt.format(startDate); return ( { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onOpen(); } }} data-request-row={item.id} sx={{ cursor: 'pointer', '&:focus-visible': { outline: '2px solid var(--bal-primary)', outlineOffset: 2 } }} > {item.counterpartyName} {dateLabel} ·{' '} {timeLabel} {deadline ? ( ) : null} ); } function ListSkeleton() { return ( {[0, 1].map((key) => ( ))} ); }