# Admin Test Checklist 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 an admin page with a manual testing checklist where each check can be marked as passed/failed with auto-timestamp. **Architecture:** Hardcoded checklist items in shared constants, Prisma model for results, Fastify API route (admin-only), React admin page with MUI Accordion grouped by section. **Tech Stack:** Prisma (SQLite), Fastify, React + MUI, @tanstack/react-query, apiClient (axios) --- ### Task 1: Prisma Migration — ChecklistResult Model **Files:** - Modify: `server/prisma/schema.prisma` (add model at end) - [ ] **Step 1: Add ChecklistResult model to schema** Append to `server/prisma/schema.prisma` after the last model: ```prisma /// Результат ручной проверки тест-чеклиста model ChecklistResult { id String @id @default(cuid()) itemKey String @unique passed Boolean 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_result ``` Expected: Migration created and applied successfully. - [ ] **Step 3: Commit** ```bash git add server/prisma/schema.prisma server/prisma/migrations/ git commit -m "feat: add ChecklistResult model for manual test checklist" ``` --- ### Task 2: Shared Constants — Checklist Items **Files:** - Create: `shared/constants/test-checklist-items.js` - Create: `shared/constants/test-checklist-items.d.ts` - [ ] **Step 1: Create the JS file with all checklist items** Create `shared/constants/test-checklist-items.js`: ```js export const TEST_CHECKLIST_ITEMS = Object.freeze([ // Авторизация { key: 'auth.register-email', section: 'Авторизация', action: 'Зарегистрироваться по email', expectedResult: 'Код приходит на почту, аккаунт создаётся' }, { key: 'auth.login-password', section: 'Авторизация', action: 'Войти по паролю', expectedResult: 'Корректный пароль пускает, неправильный — ошибка' }, { key: 'auth.oauth-vk', section: 'Авторизация', action: 'Войти через OAuth VK', expectedResult: 'Редирект на VK, callback, авторизация успешна' }, { key: 'auth.oauth-yandex', section: 'Авторизация', action: 'Войти через OAuth Yandex', expectedResult: 'Редирект на Yandex, callback, авторизация успешна' }, { key: 'auth.reset-password', section: 'Авторизация', action: 'Сбросить пароль', expectedResult: 'Письмо приходит, ссылка работает, пароль меняется' }, { key: 'auth.logout', section: 'Авторизация', action: 'Выйти из аккаунта', expectedResult: 'Сессия очищается, редирект на страницу входа' }, // Каталог и товары { key: 'catalog.homepage', section: 'Каталог и товары', action: 'Открыть главную страницу', expectedResult: 'Слайдер грузится, товары отображаются' }, { key: 'catalog.filters', section: 'Каталог и товары', action: 'Применить фильтры', expectedResult: 'Фильтры по категории, цене, материалам работают' }, { key: 'catalog.product-page', section: 'Каталог и товары', action: 'Открыть страницу товара', expectedResult: 'Фото, описание, цена, кнопка "В корзину" отображаются' }, { key: 'catalog.seo', section: 'Каталог и товары', action: 'Проверить SEO-метаданные', expectedResult: 'Title, meta, slug корректные' }, // Корзина { key: 'cart.add', section: 'Корзина', action: 'Добавить товар в корзину', expectedResult: 'Счётчик корзины обновляется' }, { key: 'cart.change-qty', section: 'Корзина', action: 'Изменить количество товара', expectedResult: 'Сумма пересчитывается' }, { key: 'cart.remove', section: 'Корзина', action: 'Удалить товар из корзины', expectedResult: 'Товар убирается, сумма пересчитывается' }, // Чекаут { key: 'checkout.address', section: 'Чекаут', action: 'Выбрать адрес доставки', expectedResult: 'Можно выбрать из сохранённых или добавить новый' }, { key: 'checkout.delivery', section: 'Чекаут', action: 'Выбрать способ доставки', expectedResult: 'Почта, OZON, Яндекс, 5post — доступны' }, { key: 'checkout.payment', section: 'Чекаут', action: 'Выбрать способ оплаты', expectedResult: 'Онлайн / при получении — доступны' }, { key: 'checkout.comment', section: 'Чекаут', action: 'Добавить комментарий к заказу', expectedResult: 'Поле работает, текст сохраняется' }, { key: 'checkout.create', section: 'Чекаут', action: 'Создать заказ', expectedResult: 'Заказ создаётся, статус DRAFT' }, // Оплата { key: 'payment.yookassa', section: 'Оплата', action: 'Оплатить через ЮKassa', expectedResult: 'Редирект на оплату, webhook обрабатывается' }, { key: 'payment.status', section: 'Оплата', action: 'Проверить статус платежа', expectedResult: 'Статус обновляется после webhook' }, // Профиль пользователя { key: 'profile.avatar', section: 'Профиль пользователя', action: 'Управление аватаром', expectedResult: 'Загрузка, отображение, удаление работают' }, { key: 'profile.settings', section: 'Профиль пользователя', action: 'Изменить настройки профиля', expectedResult: 'Email, имя, способы входа обновляются' }, { key: 'profile.addresses', section: 'Профиль пользователя', action: 'Управление адресами', expectedResult: 'Добавление, редактирование, удаление, по умолчанию' }, { key: 'profile.orders', section: 'Профиль пользователя', action: 'Просмотр заказов', expectedResult: 'Список, детали, статусы отображаются' }, { key: 'profile.messages', section: 'Профиль пользователя', action: 'Сообщения по заказу', expectedResult: 'Отправка, получение, read state работают' }, { key: 'profile.notifications', section: 'Профиль пользователя', action: 'Настройки уведомлений', expectedResult: 'Вкл/выкл каналов работают' }, { key: 'profile.delete-account', section: 'Профиль пользователя', action: 'Удалить аккаунт', expectedResult: 'Данные удаляются' }, // Админ — Товары { key: 'admin-products.list', section: 'Админ — Товары', action: 'Открыть список товаров', expectedResult: 'Пагинация, поиск работают' }, { key: 'admin-products.create', section: 'Админ — Товары', action: 'Создать товар', expectedResult: 'Все поля сохраняются, фото загружаются, публикация работает' }, { key: 'admin-products.edit', section: 'Админ — Товары', action: 'Редактировать товар', expectedResult: 'Изменения сохраняются' }, { key: 'admin-products.delete', section: 'Админ — Товары', action: 'Удалить товар', expectedResult: 'Товар удаляется' }, { key: 'admin-products.images', section: 'Админ — Товары', action: 'Управление изображениями товара', expectedResult: 'Добавление, сортировка, удаление работают' }, // Админ — Категории { key: 'admin-categories.crud', section: 'Админ — Категории', action: 'CRUD категорий', expectedResult: 'Создание, редактирование, удаление, сортировка работают' }, // Админ — Заказы { key: 'admin-orders.list', section: 'Админ — Заказы', action: 'Открыть список заказов', expectedResult: 'Фильтрация по статусу, внимание отображается' }, { key: 'admin-orders.details', section: 'Админ — Заказы', action: 'Открыть детали заказа', expectedResult: 'Состав, статус, смена статуса работают' }, { key: 'admin-orders.messages', section: 'Админ — Заказы', action: 'Ответить на сообщение заказа', expectedResult: 'Сообщение отправляется пользователю' }, // Админ — Отзывы { key: 'admin-reviews.list', section: 'Админ — Отзывы', action: 'Открыть список отзывов', expectedResult: 'Фильтрация pending/approved/rejected работает' }, { key: 'admin-reviews.moderate', section: 'Админ — Отзывы', action: 'Модерировать отзыв', expectedResult: 'Approve/reject работают' }, // Админ — Пользователи { key: 'admin-users.list', section: 'Админ — Пользователи', action: 'Открыть список пользователей', expectedResult: 'Email, дата регистрации отображаются' }, { key: 'admin-users.orders', section: 'Админ — Пользователи', action: 'Просмотр заказов пользователя', expectedResult: 'Заказы пользователя отображаются' }, // Админ — Галерея { key: 'admin-gallery.upload', section: 'Админ — Галерея', action: 'Управление галереей', expectedResult: 'Загрузка, удаление, использование в слайдере работают' }, // Админ — Настройки { key: 'admin-settings.notifications', section: 'Админ — Настройки', action: 'Настройки уведомлений админа', expectedResult: 'Email, telegram настраиваются' }, // Инфо-страницы { key: 'info.pages', section: 'Инфо-страницы', action: 'Открыть инфо-страницы', expectedResult: 'Доставка, оплата, как заказать, статусы заказов отображаются' }, { key: 'info.legal', section: 'Инфо-страницы', action: 'Открыть юридические страницы', expectedResult: 'Политика конфиденциальности, условия использования отображаются' }, // SSE / Realtime { key: 'sse.notifications', section: 'SSE / Realtime', action: 'Проверить SSE-уведомления', expectedResult: 'Уведомления приходят в реальном времени' }, ]) ``` - [ ] **Step 2: Create the TypeScript declaration file** Create `shared/constants/test-checklist-items.d.ts`: ```ts export interface TestChecklistItem { key: string section: string action: string expectedResult: string } export declare const TEST_CHECKLIST_ITEMS: readonly TestChecklistItem[] ``` - [ ] **Step 3: Commit** ```bash git add shared/constants/test-checklist-items.js shared/constants/test-checklist-items.d.ts git commit -m "feat: add test checklist items shared constants" ``` --- ### Task 3: Server API Route **Files:** - Create: `server/src/routes/api/admin/test-checklist.js` - Modify: `server/src/routes/api.js` (register new route) - [ ] **Step 1: Create the API route file** Create `server/src/routes/api/admin/test-checklist.js`: ```js import { prisma } from '../../../lib/prisma.js' export async function registerAdminTestChecklistRoutes(fastify) { fastify.get('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async () => { const results = await prisma.checklistResult.findMany() const resultMap = {} for (const r of results) { resultMap[r.itemKey] = { passed: r.passed, checkedAt: r.checkedAt.toISOString() } } return { results: resultMap } }) fastify.patch('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request) => { const { itemKey, passed } = request.body || {} if (!itemKey || typeof passed !== 'boolean') { return fastify.httpErrors.badRequest('itemKey and passed (boolean) are required') } const result = await prisma.checklistResult.upsert({ where: { itemKey }, create: { itemKey, passed }, update: { passed, checkedAt: new Date() }, }) return { result: { itemKey: result.itemKey, passed: result.passed, checkedAt: result.checkedAt.toISOString() } } }) fastify.post('/api/admin/test-checklist/reset', { preHandler: [fastify.verifyAdmin] }, async () => { await prisma.checklistResult.deleteMany({}) return { ok: true } }) } ``` - [ ] **Step 2: Register the route in api.js** Read `server/src/routes/api.js` to find where other admin routes are registered (look for imports and calls like `registerAdminNotificationRoutes`). Add import and registration following the existing pattern. Example: ```js import { registerAdminTestChecklistRoutes } from './api/admin/test-checklist.js' // ... inside the registerApiRoutes function: await registerAdminTestChecklistRoutes(fastify) ``` - [ ] **Step 3: Commit** ```bash git add server/src/routes/api/admin/test-checklist.js server/src/routes/api.js git commit -m "feat: add admin test-checklist API routes" ``` --- ### Task 4: Client — API Layer **Files:** - Create: `client/src/entities/test-checklist/api/test-checklist-api.ts` - [ ] **Step 1: Create the API functions** Create `client/src/entities/test-checklist/api/test-checklist-api.ts`: ```ts import { apiClient } from '@/shared/api/client' export interface ChecklistResultDto { passed: boolean checkedAt: string } export interface TestChecklistResponse { results: Record } export async function fetchTestChecklistResults(): Promise { const { data } = await apiClient.get('admin/test-checklist') return data } export async function updateTestChecklistItem(itemKey: string, passed: boolean): Promise<{ itemKey: string; passed: boolean; checkedAt: string }> { const { data } = await apiClient.patch<{ result: { itemKey: string; passed: boolean; checkedAt: string } }>('admin/test-checklist', { itemKey, passed }) 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 test-checklist API client functions" ``` --- ### Task 5: Client — Admin Test Checklist Page **Files:** - Create: `client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx` - Create: `client/src/pages/admin-test-checklist/index.ts` - [ ] **Step 1: Create the page component** Create `client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx`: ```tsx 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(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() 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 ( Тест-чеклист Пройдено: {passedCount} из {total} {isLoading ? ( ) : ( sections.map(([section, items]) => ( setExpanded(isExpanded ? section : false)}> }> {section} Действие Ожидаемый результат Дата проверки {items.map((item) => { const r = results[item.key] return ( updateMutation.mutate({ itemKey: item.key, passed: checked })} color="success" /> {item.action} {item.expectedResult} {r ? formatDate(r.checkedAt) : '—'} ) })}
)) )} setConfirmOpen(false)}> Сбросить все проверки? Все отметки будут удалены. Это действие нельзя отменить.
) } ``` - [ ] **Step 2: Create the barrel export** Create `client/src/pages/admin-test-checklist/index.ts`: ```ts export { AdminTestChecklistPage } from './ui/AdminTestChecklistPage' ``` - [ ] **Step 3: Commit** ```bash git add client/src/pages/admin-test-checklist/ git commit -m "feat: add admin test checklist page" ``` --- ### Task 6: Client — Routing & Navigation **Files:** - Modify: `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx` (add nav item + route) - [ ] **Step 1: Add nav item and route to AdminLayoutPage** In `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx`: 1. Add import for `ClipboardCheck` from `lucide-react`: ```tsx import { Bell, ClipboardCheck, Image, LayoutGrid, ListOrdered, MessageSquare, Settings, Store, Users } from 'lucide-react' ``` 2. Add import for the page: ```tsx import { AdminTestChecklistPage } from '@/pages/admin-test-checklist' ``` 3. Add nav item to `navItems` array (after настройки): ```tsx { to: '/admin/test-checklist', label: 'Тест-чеклист', icon: }, ``` 4. Add route to `` (before the catch-all `*`): ```tsx } /> ``` - [ ] **Step 2: Commit** ```bash git add client/src/pages/admin-layout/ui/AdminLayoutPage.tsx git commit -m "feat: add test-checklist to admin navigation and routing" ``` --- ### Task 7: Verification - [ ] **Step 1: Run server tests** ```bash cd /mnt/d/my_projects/shop/server && npm test ``` Expected: All tests pass. - [ ] **Step 2: Run client lint** ```bash cd /mnt/d/my_projects/shop/client && npm run lint ``` Expected: No 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 tests** ```bash cd /mnt/d/my_projects/shop/client && npm test ``` Expected: All tests pass. - [ ] **Step 5: Run client build** ```bash cd /mnt/d/my_projects/shop/client && npm run build ``` Expected: Build succeeds with no type errors.