# Test Checklist v2 — 3-State Status + Error Comments Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add "failed with error" state to checklist items with a mandatory error comment dialog. **Architecture:** Add `comment String?` field to `ChecklistResult` model. Update PATCH API to accept optional comment. Replace checkbox with 3-state status selector (passed/failed/not-checked). Show error comment dialog when marking as failed. **Tech Stack:** Prisma (SQLite), Fastify, React + MUI, @tanstack/react-query, apiClient (axios) --- ### Task 1: Prisma Migration — Add comment field **Files:** - Modify: `server/prisma/schema.prisma` - [ ] **Step 1: Add comment field to ChecklistResult model** In `server/prisma/schema.prisma`, find the `ChecklistResult` model and add `comment String?` after `passed`: ```prisma /// Результат ручной проверки тест-чеклиста model ChecklistResult { id String @id @default(cuid()) itemKey String @unique passed Boolean comment String? checkedAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` - [ ] **Step 2: Run migration** ```bash cd /mnt/d/my_projects/shop/server && npx prisma migrate dev --name add_checklist_comment ``` Expected: Migration created and applied successfully. - [ ] **Step 3: Commit** ```bash git add server/prisma/schema.prisma server/prisma/migrations/ git commit -m "feat: add comment field to ChecklistResult for error descriptions" ``` --- ### Task 2: Server API — Support comment in PATCH and GET **Files:** - Modify: `server/src/routes/api/admin/test-checklist.js` - [ ] **Step 1: Update PATCH to accept and store comment** Replace the PATCH handler in `server/src/routes/api/admin/test-checklist.js`: ```js fastify.patch('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const { itemKey, passed, comment } = request.body || {} if (!itemKey || typeof passed !== 'boolean') { return reply.code(400).send({ error: 'itemKey и passed (boolean) обязательны' }) } if (comment !== undefined && typeof comment !== 'string') { return reply.code(400).send({ error: 'comment должен быть строкой' }) } if (comment !== undefined && comment.length > 2000) { return reply.code(400).send({ error: 'Комментарий слишком длинный (макс. 2000 символов)' }) } const result = await prisma.checklistResult.upsert({ where: { itemKey }, create: { itemKey, passed, comment: passed ? null : comment || null }, update: { passed, comment: passed ? null : comment ?? undefined, checkedAt: new Date() }, }) return { result: { itemKey: result.itemKey, passed: result.passed, comment: result.comment, checkedAt: result.checkedAt.toISOString() } } }) ``` - [ ] **Step 2: Update GET to include comment in response** Replace the GET handler's result mapping: ```js fastify.get('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const results = await prisma.checklistResult.findMany() const resultMap = {} for (const r of results) { resultMap[r.itemKey] = { passed: r.passed, comment: r.comment, checkedAt: r.checkedAt.toISOString() } } return { results: resultMap } }) ``` - [ ] **Step 3: Commit** ```bash git add server/src/routes/api/admin/test-checklist.js git commit -m "feat: support comment field in test-checklist API" ``` --- ### Task 3: Client API Layer — Add comment to types and functions **Files:** - Modify: `client/src/entities/test-checklist/api/test-checklist-api.ts` - [ ] **Step 1: Update types and update function** Replace the entire file content: ```ts import { apiClient } from '@/shared/api/client' export type ChecklistResultDto = { passed: boolean comment: string | null checkedAt: string } export type TestChecklistResponse = { results: Record } export type UpdateChecklistItemResponse = { itemKey: string passed: boolean comment: string | null checkedAt: string } export async function fetchTestChecklistResults(): Promise { const { data } = await apiClient.get('admin/test-checklist') return data } export async function updateTestChecklistItem( itemKey: string, passed: boolean, comment?: string | null, ): Promise { const { data } = await apiClient.patch<{ result: UpdateChecklistItemResponse }>('admin/test-checklist', { itemKey, passed, comment: passed ? null : comment ?? null, }) return data.result } export async function resetTestChecklist(): Promise { await apiClient.post('admin/test-checklist/reset') } ``` - [ ] **Step 2: Commit** ```bash git add client/src/entities/test-checklist/api/test-checklist-api.ts git commit -m "feat: add comment to test-checklist API client types and function" ``` --- ### Task 4: Client Page — 3-state status + error comment dialog **Files:** - Modify: `client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx` - [ ] **Step 1: Replace the entire page component** Replace the entire file content: ```tsx import { useCallback, useMemo, useState } from 'react' import Alert from '@mui/material/Alert' 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 CircularProgress from '@mui/material/CircularProgress' import Dialog from '@mui/material/Dialog' import DialogActions from '@mui/material/DialogActions' import DialogContent from '@mui/material/DialogContent' import DialogTitle from '@mui/material/DialogTitle' import IconButton from '@mui/material/IconButton' 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 TextField from '@mui/material/TextField' import Tooltip from '@mui/material/Tooltip' import Typography from '@mui/material/Typography' import CheckCircleIcon from '@mui/icons-material/CheckCircle' import CancelIcon from '@mui/icons-material/Cancel' 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' type Status = 'passed' | 'failed' | 'unchecked' function statusFromResult(r: { passed: boolean; comment: string | null } | undefined): Status { if (!r) return 'unchecked' return r.passed ? 'passed' : 'failed' } 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', }) } function StatusIcon({ status, onClick }: { status: Status; onClick: () => void }) { const icons = { passed: , failed: , unchecked: , } return ( {icons[status]} ) } export function AdminTestChecklistPage() { const queryClient = useQueryClient() const [confirmOpen, setConfirmOpen] = useState(false) const [expanded, setExpanded] = useState(false) const [errorDialogOpen, setErrorDialogOpen] = useState(false) const [errorItemKey, setErrorItemKey] = useState(null) const [errorComment, setErrorComment] = useState('') const [errorCommentError, setErrorCommentError] = useState(false) const { data, isLoading, isError } = useQuery({ queryKey: ['admin', 'test-checklist'], queryFn: fetchTestChecklistResults, }) const updateMutation = useMutation({ mutationFn: ({ itemKey, passed, comment }: { itemKey: string; passed: boolean; comment?: string | null }) => updateTestChecklistItem(itemKey, passed, comment), 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() 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 const handleStatusClick = useCallback( (itemKey: string) => { const current = statusFromResult(results[itemKey]) if (current === 'failed') { // Clicking on failed → reset to unchecked updateMutation.mutate({ itemKey, passed: false, comment: null }) } else if (current === 'unchecked') { // Clicking on unchecked → mark as passed updateMutation.mutate({ itemKey, passed: true }) } else { // Clicking on passed → open error dialog setErrorItemKey(itemKey) setErrorComment('') setErrorCommentError(false) setErrorDialogOpen(true) } }, [results, updateMutation], ) const handleSaveError = () => { if (!errorItemKey) return if (!errorComment.trim()) { setErrorCommentError(true) return } updateMutation.mutate({ itemKey: errorItemKey, passed: false, comment: errorComment.trim() }) setErrorDialogOpen(false) setErrorItemKey(null) setErrorComment('') } const errorItem = errorItemKey ? TEST_CHECKLIST_ITEMS.find((i) => i.key === errorItemKey) : null return ( Тест-чеклист Пройдено: {passedCount} из {total} {isLoading ? ( ) : isError ? ( Не удалось загрузить чеклист. ) : ( sections.map(([section, items]) => ( setExpanded(isExpanded ? section : false)} > }> {section} Действие Ожидаемый результат Комментарий Дата проверки {items.map((item) => { const r = results[item.key] const status = statusFromResult(r) return ( handleStatusClick(item.key)} /> {item.action} {item.expectedResult} {r?.comment ? ( {r.comment} ) : ( )} {r ? formatDate(r.checkedAt) : '—'} ) })}
)) )} setConfirmOpen(false)}> Сбросить все проверки? Все отметки будут удалены. Это действие нельзя отменить. setErrorDialogOpen(false)} maxWidth="sm" fullWidth> Описание ошибки — {errorItem?.action} { setErrorComment(e.target.value) if (errorCommentError) setErrorCommentError(false) }} placeholder="Опишите, что пошло не так..." error={errorCommentError} helperText={errorCommentError ? 'Обязательное поле' : ''} sx={{ mt: 1 }} />
) } ``` - [ ] **Step 2: Commit** ```bash git add client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx git commit -m "feat: add 3-state status and error comment dialog to test checklist" ``` --- ### Task 5: Verification - [ ] **Step 1: Run server tests** ```bash cd /mnt/d/my_projects/shop/server && npm test ``` Expected: All tests pass (pre-existing failures unrelated). - [ ] **Step 2: Run client lint** ```bash cd /mnt/d/my_projects/shop/client && npm run lint ``` Expected: 0 errors. - [ ] **Step 3: Run client format check** ```bash cd /mnt/d/my_projects/shop/client && npm run format:check ``` Expected: All files formatted correctly. - [ ] **Step 4: Run client build** ```bash cd /mnt/d/my_projects/shop/client && npm run build ``` Expected: Build succeeds with no type errors.