feat: add admin test checklist page
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user