'use client'; import { Suspense, useState } from 'react'; import { useTranslations } from 'next-intl'; import { useSnackbar } from 'notistack'; import { Box, Chip, MenuItem, Paper, Skeleton, Stack, TextField, Typography } from '@mui/material'; import { AppButton, AppLoading, RatingInput } from '@/components'; import { AdminEmptyState, AdminErrorState, AdminPageHeader, AdminPager, ConfirmDialog } from '@/components/admin'; import { useAdminCapabilities, useAdminListState } from '@/hooks'; import { REVIEWS_PAGE_SIZE } from '@/services/reviews/constants'; import type { ModerationAction, ModerationQueueItem, ModerationStatus } from '@/services/reviews/types'; import { useModerationQueue, useModerateReview } from '@/services/reviews'; /** The moderation worklist tabs — the four `moderationStatus` values; the queue defaults to the pending backlog. */ const MODERATION_STATUSES: readonly ModerationStatus[] = ['pending_moderation', 'published', 'hidden', 'rejected']; /** Status → MUI chip color. **Never** styles `pending_moderation` like a published review (it reads as a warning). */ const STATUS_CHIP_COLOR: Record = { pending_moderation: 'warning', published: 'success', hidden: 'default', rejected: 'error', }; /** * Review moderation queue (f15) — the admin worklist over `reviews` awaiting a decision (b14). Each row carries * moderation internals (`lowRatingAlertId`, the nurse/booking context) that are **never** rendered on a * customer/nurse surface. A review is born `pending_moderation` and is never public / never counted until an * admin publishes it, so the card presents pending content as an under-review item, not as a published review. * Publishing recomputes the nurse aggregate **server-side**; the mutation invalidates the queue so the row * leaves on success. `canModerate` only hides the controls — the server enforces the role scope. */ const DEFAULT_STATUS: ModerationStatus = 'pending_moderation'; function parseFilters(params: URLSearchParams): { status: ModerationStatus } { const status = params.get('status') as ModerationStatus | null; return { status: status && MODERATION_STATUSES.includes(status) ? status : DEFAULT_STATUS }; } export default function AdminReviewsPage() { return ( }> ); } function AdminReviewsPageInner() { const t = useTranslations('admin'); const caps = useAdminCapabilities(); const { enqueueSnackbar } = useSnackbar(); const listState = useAdminListState<{ status: ModerationStatus }>({ parse: parseFilters, serialize: (f): Record => (f.status !== DEFAULT_STATUS ? { status: f.status } : {}), empty: { status: DEFAULT_STATUS }, }); const status = listState.applied.status; const page = listState.page; const [pending, setPending] = useState<{ item: ModerationQueueItem; action: ModerationAction } | null>(null); const queue = useModerationQueue({ status }, page); const moderate = useModerateReview(); const items = queue.data?.items ?? []; const pageCount = Math.max(1, Math.ceil((queue.data?.total ?? 0) / REVIEWS_PAGE_SIZE)); const setStatus = (next: ModerationStatus) => listState.applyFilters({ status: next }); const requireReason = pending?.action === 'hide' || pending?.action === 'reject'; const onConfirm = (reason?: string) => { if (!pending) return; moderate.mutate( { reviewId: pending.item.id, action: pending.action, reason }, { onSuccess: () => { enqueueSnackbar(t('mod_done'), { variant: 'success' }); setPending(null); }, }, ); }; return ( setStatus(e.target.value as ModerationStatus)} sx={{ minWidth: 180 }} > {MODERATION_STATUSES.map((s) => ( {t(`mstatus_${s}`)} ))} } /> {queue.isLoading ? ( {[0, 1, 2].map((k) => )} ) : queue.isError ? ( queue.refetch()} /> ) : items.length === 0 ? ( ) : ( {items.map((item) => ( setPending({ item, action })} /> ))} )} listState.goToPage(Math.max(1, page - 1))} onNext={() => listState.goToPage(Math.min(pageCount, page + 1))} prevLabel={t('prev_page')} nextLabel={t('next_page')} indicator={t('page_indicator', { page, total: pageCount })} /> setPending(null)} /> ); } /** One review awaiting a decision. Presentational; the actions bubble up to the page-level confirm dialog. */ function ModerationCard({ item, canModerate, onAct, }: { item: ModerationQueueItem; canModerate: boolean; onAct: (action: ModerationAction) => void; }) { const t = useTranslations('admin'); const tReviews = useTranslations('reviews'); return ( {item.lowRatingAlertId != null ? ( ) : null} {item.body ? ( {item.body} ) : null} {item.tagCodes.length > 0 ? ( {item.tagCodes.map((code) => ( ))} ) : null} {t('mod_nurse', { id: item.nurseProfileId })} {t('mod_booking', { id: item.bookingId })} {canModerate ? ( onAct('publish')}> {t('mod_publish')} onAct('hide')}> {t('mod_hide')} onAct('reject')}> {t('mod_reject')} ) : null} ); }