feat: add admin test checklist page

This commit is contained in:
Kirill
2026-05-24 16:27:14 +05:00
parent 873d98eb1e
commit 69f7e4f9e8
2 changed files with 157 additions and 0 deletions
@@ -0,0 +1 @@
export { AdminTestChecklistPage } from './ui/AdminTestChecklistPage'
@@ -0,0 +1,156 @@
import { useMemo, useState } from 'react'
import Accordion from '@mui/material/Accordion'
import AccordionDetails from '@mui/material/AccordionDetails'
import AccordionSummary from '@mui/material/AccordionSummary'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Checkbox from '@mui/material/Checkbox'
import CircularProgress from '@mui/material/CircularProgress'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
import Stack from '@mui/material/Stack'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import Typography from '@mui/material/Typography'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { TEST_CHECKLIST_ITEMS } from '@shared/constants/test-checklist-items'
import { fetchTestChecklistResults, resetTestChecklist, updateTestChecklistItem } from '@/entities/test-checklist/api/test-checklist-api'
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
export function AdminTestChecklistPage() {
const queryClient = useQueryClient()
const [confirmOpen, setConfirmOpen] = useState(false)
const [expanded, setExpanded] = useState<string | false>(false)
const { data, isLoading } = useQuery({
queryKey: ['admin', 'test-checklist'],
queryFn: fetchTestChecklistResults,
})
const updateMutation = useMutation({
mutationFn: ({ itemKey, passed }: { itemKey: string; passed: boolean }) => updateTestChecklistItem(itemKey, passed),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
},
})
const resetMutation = useMutation({
mutationFn: resetTestChecklist,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
setConfirmOpen(false)
},
})
const sections = useMemo(() => {
const map = new Map<string, typeof TEST_CHECKLIST_ITEMS>()
for (const item of TEST_CHECKLIST_ITEMS) {
const existing = map.get(item.section) || []
existing.push(item)
map.set(item.section, existing)
}
return Array.from(map.entries())
}, [])
const results = data?.results ?? {}
const total = TEST_CHECKLIST_ITEMS.length
const passedCount = TEST_CHECKLIST_ITEMS.filter((i) => results[i.key]?.passed).length
return (
<Box>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 3 }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700 }}>
Тест-чеклист
</Typography>
<Typography variant="body2" color="text.secondary">
Пройдено: {passedCount} из {total}
</Typography>
</Box>
<Button variant="outlined" color="warning" onClick={() => setConfirmOpen(true)} disabled={resetMutation.isPending}>
Сбросить все
</Button>
</Stack>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
) : (
sections.map(([section, items]) => (
<Accordion key={section} expanded={expanded === section} onChange={(_, isExpanded) => setExpanded(isExpanded ? section : false)}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography sx={{ fontWeight: 600 }}>{section}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: 0 }}>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell padding="checkbox" sx={{ width: 56 }} />
<TableCell sx={{ fontWeight: 600 }}>Действие</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Ожидаемый результат</TableCell>
<TableCell sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}>Дата проверки</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => {
const r = results[item.key]
return (
<TableRow key={item.key} hover>
<TableCell padding="checkbox">
<Checkbox
checked={r?.passed ?? false}
onChange={(_, checked) => updateMutation.mutate({ itemKey: item.key, passed: checked })}
color="success"
/>
</TableCell>
<TableCell>{item.action}</TableCell>
<TableCell>{item.expectedResult}</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap', color: r ? 'text.primary' : 'text.disabled' }}>
{r ? formatDate(r.checkedAt) : '—'}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))
)}
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
<DialogTitle>Сбросить все проверки?</DialogTitle>
<DialogContent>
<DialogContentText>Все отметки будут удалены. Это действие нельзя отменить.</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmOpen(false)}>Отмена</Button>
<Button color="warning" onClick={() => resetMutation.mutate()} autoFocus>
Сбросить
</Button>
</DialogActions>
</Dialog>
</Box>
)
}