# Комментарии к документу в UI — план реализации > **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:** список комментариев документа на экране проверки [41] (боковая панель) и счётчик с поповером в панели «Первичные документы» [39-01]. **Architecture:** только фронт `web/`; API ядра готов (`GET`/`POST /documents/{id}/comments`, `comments_count`). Общий компонент `CommentsList` используют боковая панель экрана документа и поповер строки панели. Чистые функции (форматирование времени, строки панели) — с тестами vitest; компоненты проверяются в браузере. **Tech Stack:** React 18, TS strict, antd 6, vitest, biome + ESLint + Prettier. **Spec:** `openspec/changes/document-comments-ui/` (proposal, design D-1…D-5, дельты `document-review-ui`, `primary-files-ui`). **Ветка:** `feat/180-document-comments` (worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/doc-comments/arm-core`), issue RSHB/arm-core#180, Jira AURORA-1683. ## Global Constraints - Тексты UI: «Комментарии (N)», «Комментарии к документу», «Комментариев нет», «Не удалось загрузить комментарии», «Добавить», «Комментарии к документу <имя файла>», «Комментарий отсутствует», «Закрыть». - Время — по Москве, «dd.mm.yyyy hh:mm». - Текст комментария — до 4000 символов; пустой после обрезки не отправляется. - Правок и удаления комментариев нет; бэкенд и DBML не меняются. - Стиль: табы, одинарные кавычки, ширина 140 (biome); комментарии в коде — по-русски. ## Review Focus - Клик по кнопке комментариев и внутри поповера в строке панели не должен открывать документ (всплытие к `onRow.onClick`) — проверка в браузере, Task 3. - Длинный текст без пробелов и многострочный текст не ломают ширину панели/поповера (`pre-wrap` + `overflow-wrap: anywhere`) — проверка в браузере, Task 4. - Ответ на запрос списка от прошлого открытия, пришедший после закрытия или переоткрытия, не должен перетирать текущий (счётчик запросов, как в `FileWarningsPopover`) — Task 2/3. - Ошибка добавления (403/422) оставляет текст в поле — Task 2, проверка в браузере. - Ядро без `comments_count` в документе — счётчик 0 (тест в Task 1). --- ### Task 1: Данные — тип, запрос, время, счётчик строки **Files:** - Modify: `web/src/types/api.ts` (после `DocumentBrief`) - Modify: `web/src/services/api/documents.ts` - Modify: `web/src/utils/dates.ts`, Test: `web/src/utils/dates.test.ts` - Modify: `web/src/widgets/FilesDrawer/documentRows.ts`, Test: `web/src/widgets/FilesDrawer/documentRows.test.ts` **Interfaces:** - Produces: `interface CommentOut { id: string; text: string; author: { id: string; full_name: string }; created_at: string }`; `getComments(documentId: string): Promise`; `postComment` типизируется `api.post`; `formatMoscowDateTime(iso: string | null | undefined): string | null`; `DocumentRow.commentsCount: number`. - [ ] **Step 1: Падающие тесты** ```ts describe('formatMoscowDateTime', () => { it('момент из API (UTC) — дата и время по Москве', () => { expect(formatMoscowDateTime('2026-10-07T23:11:42.981145Z')).toBe('08.10.2026 02:11'); expect(formatMoscowDateTime('2024-03-12T10:05:00Z')).toBe('12.03.2024 13:05'); }); it('пусто и мусор', () => { expect(formatMoscowDateTime(null)).toBeNull(); expect(formatMoscowDateTime('не дата')).toBeNull(); }); }); ``` В `documentRows.test.ts`: `buildDocumentRows([file({ documents: [doc({ comments_count: 2 })] })])[0].commentsCount === 2`; строка файла без документов — `commentsCount === 0`; документ без поля (`{ ...doc(), comments_count: undefined } as unknown as DocumentBrief`) — 0. - [ ] **Step 2:** `cd web && npx vitest run src/utils/dates.test.ts src/widgets/FilesDrawer/documentRows.test.ts` — FAIL (нет функции/поля). - [ ] **Step 3: Реализация.** `formatMoscowDateTime` — `Intl.DateTimeFormat('ru-RU', { timeZone: 'Europe/Moscow', day, month, year, hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })`, разделитель «, » из `Intl` заменить пробелом. `commentsCount: d.comments_count ?? 0`, у строки файла — 0. `getComments` — `api.get(\`/documents/${id}/comments\`)`. - [ ] **Step 4:** тот же vitest — PASS; `npx tsc --noEmit` — без ошибок. - [ ] **Step 5: Commit** «AURORA-1683 комментарии: тип, запрос, время по Москве, счётчик строки панели». ### Task 2: Экран документа — кнопка «Комментарии (N)» и боковая панель **Files:** - Create: `web/src/widgets/DocumentComments/CommentsList.tsx` - Create: `web/src/widgets/DocumentComments/useDocumentComments.ts` - Create: `web/src/widgets/DocumentComments/DocumentCommentsDrawer.tsx` - Modify: `web/src/layouts/DocumentPage.tsx:1-136` - Delete: `web/src/widgets/DocumentReview/CommentModal.tsx` **Interfaces:** - Consumes: `CommentOut`, `getComments`, `postComment`, `formatMoscowDateTime` (Task 1). - Produces: `type CommentsState = { status: 'loading' } | { status: 'error' } | { status: 'done'; comments: CommentOut[] }`; `CommentsList({ state, emptyText }: { state: CommentsState | null; emptyText: string })`; `useDocumentComments(documentId: string | null): { state: CommentsState | null; load: () => void; reset: () => void }` — `load` запрашивает список, ответы устаревших запросов игнорируются (счётчик в `useRef`), `reset` сбрасывает в `null` и отменяет ожидаемый ответ; `DocumentCommentsDrawer({ documentId, open, onClose, onAdded }: { documentId: string; open: boolean; onClose: () => void; onAdded: () => void })`. - [ ] **Step 1:** `CommentsList`: `null`/loading — `Spin`; error — «Не удалось загрузить комментарии» (цвет `#cf1322`); пусто — `emptyText` (цвет `#8c8c8c`); иначе на комментарий блок: ФИО жирным + время `formatMoscowDateTime` (secondary, 12px) в строке, текст ниже с `whiteSpace: 'pre-wrap', overflowWrap: 'anywhere'`; ключ — `id`. - [ ] **Step 2:** `DocumentCommentsDrawer`: antd `Drawer` справа, `title='Комментарии к документу'`, ширина 420; при `open` — `load()`, при закрытии — `reset()`. Тело — `CommentsList` (`emptyText='Комментариев нет'`) с прокруткой, футер — `Input.TextArea` (`maxLength={4000}`, `showCount`, `autoSize={{ minRows: 3, maxRows: 8 }}`) и кнопка «Добавить» (`disabled` при пустом `trim()`, `loading` во время запроса). Успех: очистить поле, `message.success('Комментарий добавлен')`, `load()`, `onAdded()`. Ошибка: `message.error(e instanceof ApiError ? e.message : 'Не удалось добавить комментарий')`, текст остаётся. - [ ] **Step 3:** `DocumentPage`: кнопка `` на месте «Добавить комментарий»; `DocumentCommentsDrawer` вместо `CommentModal`, `onAdded={load}` (перечитывает документ — счётчик). Удалить `CommentModal.tsx` и его импорт. - [ ] **Step 4:** `cd web && npx tsc --noEmit && npm run lint && npx biome check src` — чисто; `npm test` — зелёный. - [ ] **Step 5: Commit** «AURORA-1683 экран документа: список комментариев в боковой панели». ### Task 3: Панель «Первичные документы» — счётчик и поповер **Files:** - Create: `web/src/widgets/DocumentComments/CommentsPopover.tsx` - Modify: `web/src/widgets/FilesDrawer/FilesList.tsx:136-150` (колонка `actions`) **Interfaces:** - Consumes: `useDocumentComments`, `CommentsList` (Task 2), `DocumentRow.commentsCount` (Task 1). - Produces: `CommentsPopover({ documentId, fileName, count }: { documentId: string; fileName: string; count: number })`. - [ ] **Step 1:** `CommentsPopover`: управляемый antd `Popover` (`trigger='click'`, `open`/`onOpenChange`), `title={\`Комментарии к документу ${fileName}\`}`; при открытии `load()`, при закрытии `reset()`. Содержимое — обёртка с `onClick`/`onKeyDown` `stopPropagation` (как в `FileWarningsPopover`), `maxWidth: 420, maxHeight: 320, overflowY: 'auto'`, `CommentsList` (`emptyText='Комментарий отсутствует'`), справа внизу `Button size='small'` «Закрыть». Триггер — `Button` с `CommentOutlined` и `{count > 0 ? count : null}`, `aria-label={\`Комментарии: ${count}\`}`, `onClick` с `stopPropagation`. - [ ] **Step 2:** `FilesList`, колонка `actions`: `
` — `CommentsPopover` (только при `r.documentId`), затем существующая кнопка «Удалить». - [ ] **Step 3:** `npx tsc --noEmit && npm run lint && npx biome check src && npm test` — чисто. - [ ] **Step 4: Commit** «AURORA-1683 панель документов: счётчик и поповер комментариев». ### Task 4: Проверка в браузере, сверка с макетом, OpenSpec **Files:** - Create: `openspec/changes/document-comments-ui/tasks.md` - [ ] **Step 1:** Стенд по `docs/dev/qa-guide.md` (api + web из worktree). Окно 1600×1000. Сценарии спеки: добавить комментарий на экране документа → «Комментарии (1)», после F5 он виден; ошибка (например, остановить api) — текст в поле остаётся; многострочный и длинный текст без пробелов не ломает панель; в панели «Первичные документы» число у иконки, поповер со списком, клик по иконке и внутри поповера не открывает документ; документ без комментариев — «Комментарий отсутствует»; замена документа с причиной → причина в комментариях заменённого. - [ ] **Step 2:** Скриншоты сравнить с `../docs/mockup/screens/39-01-edit-documents.png` и `41-document-check.png`; расхождения — списком в отчёт. - [ ] **Step 3:** `tasks.md` change (группы по задачам плана, отмеченные выполненными); `openspec validate document-comments-ui --strict` — valid. - [ ] **Step 4:** `make web-check` из корня репозитория — зелёный (вывод — в описание PR). - [ ] **Step 5: Commit** «AURORA-1683 OpenSpec: tasks, проверки».