ui phase 11

This commit is contained in:
hamid
2026-07-19 19:19:44 +03:30
parent b4b8c9ea79
commit 87fa4cd497
74 changed files with 3115 additions and 506 deletions
@@ -1,11 +1,11 @@
'use client';
import { useState } from 'react';
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, RatingInput } from '@/components';
import { AppButton, AppLoading, RatingInput } from '@/components';
import { AdminEmptyState, AdminErrorState, AdminPageHeader, AdminPager, ConfirmDialog } from '@/components/admin';
import { useAdminCapabilities } from '@/hooks';
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';
@@ -29,13 +29,33 @@ const STATUS_CHIP_COLOR: Record<ModerationStatus, 'default' | 'success' | 'warni
* 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 (
<Suspense fallback={<AppLoading />}>
<AdminReviewsPageInner />
</Suspense>
);
}
function AdminReviewsPageInner() {
const t = useTranslations('admin');
const caps = useAdminCapabilities();
const { enqueueSnackbar } = useSnackbar();
const [status, setStatus] = useState<ModerationStatus>('pending_moderation');
const [page, setPage] = useState(1);
const listState = useAdminListState<{ status: ModerationStatus }>({
parse: parseFilters,
serialize: (f): Record<string, string> => (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);
@@ -44,6 +64,8 @@ export default function AdminReviewsPage() {
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) => {
@@ -70,10 +92,7 @@ export default function AdminReviewsPage() {
size="small"
label={t('filter_label')}
value={status}
onChange={(e) => {
setStatus(e.target.value as ModerationStatus);
setPage(1);
}}
onChange={(e) => setStatus(e.target.value as ModerationStatus)}
sx={{ minWidth: 180 }}
>
{MODERATION_STATUSES.map((s) => (
@@ -107,11 +126,11 @@ export default function AdminReviewsPage() {
<AdminPager
page={page}
pageCount={pageCount}
onPrev={() => setPage((p) => Math.max(1, p - 1))}
onNext={() => setPage((p) => Math.min(pageCount, p + 1))}
onPrev={() => 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 })}
indicator={t('page_indicator', { page, total: pageCount })}
/>
<ConfirmDialog