diff --git a/web/src/layouts/DocumentPage.tsx b/web/src/layouts/DocumentPage.tsx
index dda82ca..ff3a110 100644
--- a/web/src/layouts/DocumentPage.tsx
+++ b/web/src/layouts/DocumentPage.tsx
@@ -11,7 +11,7 @@ import { getDocument } from '../services/api/documents';
import { getFile, listLegalEntityFiles } from '../services/api/files';
import type { DocumentOut } from '../types/api';
import { documentTypeLabel, periodLabel } from '../utils/labels';
-import CommentModal from '../widgets/DocumentReview/CommentModal';
+import { DocumentCommentsDrawer } from '../widgets/DocumentComments/DocumentCommentsDrawer';
import { OsvDocument } from '../widgets/DocumentReview/OsvDocument';
import { StatementReview } from '../widgets/DocumentReview/StatementReview';
import { documentPageTitle, documentSubtitle, optionStatusBadge, statusBadge } from '../widgets/DocumentReview/reviewView';
@@ -116,7 +116,7 @@ export function DocumentPage() {
/>
} onClick={() => setCommentOpen(true)}>
- Добавить комментарий
+ Комментарии ({doc.comments_count})
}
{kind === 'none' && }
- setCommentOpen(false)} />
+ setCommentOpen(false)} onAdded={load} />
);
}
diff --git a/web/src/widgets/DocumentComments/CommentsList.tsx b/web/src/widgets/DocumentComments/CommentsList.tsx
new file mode 100644
index 0000000..c5614ac
--- /dev/null
+++ b/web/src/widgets/DocumentComments/CommentsList.tsx
@@ -0,0 +1,25 @@
+import { Spin, Typography } from 'antd';
+import { formatMoscowDateTime } from '../../utils/dates';
+import type { CommentsState } from './useDocumentComments';
+
+/** Список комментариев документа: автор, дата и время по Москве, текст с переносами строк. */
+export function CommentsList({ state, emptyText }: { state: CommentsState | null; emptyText: string }) {
+ if (state === null || state.status === 'loading') return ;
+ if (state.status === 'error') return Не удалось загрузить комментарии;
+ if (state.comments.length === 0) return {emptyText};
+ return (
+
+ {state.comments.map((c) => (
+
+
+ {c.author.full_name}
+
+ {formatMoscowDateTime(c.created_at)}
+
+
+
{c.text}
+
+ ))}
+
+ );
+}
diff --git a/web/src/widgets/DocumentComments/DocumentCommentsDrawer.tsx b/web/src/widgets/DocumentComments/DocumentCommentsDrawer.tsx
new file mode 100644
index 0000000..ece504e
--- /dev/null
+++ b/web/src/widgets/DocumentComments/DocumentCommentsDrawer.tsx
@@ -0,0 +1,63 @@
+import { Button, Drawer, Input, message } from 'antd';
+import { useEffect, useState } from 'react';
+import { ApiError } from '../../api/client';
+import { postComment } from '../../services/api/documents';
+import { CommentsList } from './CommentsList';
+import { useDocumentComments } from './useDocumentComments';
+
+type DocumentCommentsDrawerProps = { documentId: string; open: boolean; onClose: () => void; onAdded: () => void };
+
+/** Боковая панель экрана документа: список комментариев и добавление нового (правки и удаления нет). */
+export function DocumentCommentsDrawer({ documentId, open, onClose, onAdded }: DocumentCommentsDrawerProps) {
+ const { state, load, reset } = useDocumentComments(documentId);
+ const [text, setText] = useState('');
+ const [saving, setSaving] = useState(false);
+
+ // список — при каждом открытии; при закрытии сбрасываем, чтобы не показать чужой список после смены документа
+ useEffect(() => {
+ if (open) load();
+ else reset();
+ }, [open, load, reset]);
+
+ const handleAdd = async () => {
+ setSaving(true);
+ try {
+ await postComment(documentId, text.trim());
+ message.success('Комментарий добавлен');
+ setText('');
+ load();
+ onAdded();
+ } catch (e: unknown) {
+ // текст в поле не трогаем: можно поправить и повторить
+ message.error(e instanceof ApiError ? e.message : 'Не удалось добавить комментарий');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+ setText(e.target.value)}
+ autoSize={{ minRows: 3, maxRows: 8 }}
+ maxLength={4000}
+ showCount
+ placeholder='Опишите результат проверки или внесенные правки'
+ />
+
+
+ }>
+
+
+ );
+}
diff --git a/web/src/widgets/DocumentComments/useDocumentComments.ts b/web/src/widgets/DocumentComments/useDocumentComments.ts
new file mode 100644
index 0000000..0f685d6
--- /dev/null
+++ b/web/src/widgets/DocumentComments/useDocumentComments.ts
@@ -0,0 +1,30 @@
+import { useCallback, useRef, useState } from 'react';
+import { getComments } from '../../services/api/documents';
+import type { CommentOut } from '../../types/api';
+
+export type CommentsState = { status: 'loading' } | { status: 'error' } | { status: 'done'; comments: CommentOut[] };
+
+/** Комментарии документа по запросу: `load` перечитывает список, `reset` сбрасывает его (при закрытии панели или поповера). */
+export function useDocumentComments(documentId: string | null) {
+ const [state, setState] = useState(null);
+ // номер последнего запроса: ответ устаревшего запроса или пришедший после reset игнорируется
+ const requestNo = useRef(0);
+
+ const load = useCallback(() => {
+ requestNo.current += 1;
+ const mine = requestNo.current;
+ if (!documentId) return;
+ // при перечитывании после добавления старый список остаётся на экране до ответа
+ setState((prev) => (prev?.status === 'done' ? prev : { status: 'loading' }));
+ getComments(documentId)
+ .then((comments) => mine === requestNo.current && setState({ status: 'done', comments }))
+ .catch(() => mine === requestNo.current && setState({ status: 'error' }));
+ }, [documentId]);
+
+ const reset = useCallback(() => {
+ requestNo.current += 1;
+ setState(null);
+ }, []);
+
+ return { state, load, reset };
+}
diff --git a/web/src/widgets/DocumentReview/CommentModal.tsx b/web/src/widgets/DocumentReview/CommentModal.tsx
deleted file mode 100644
index bfc491c..0000000
--- a/web/src/widgets/DocumentReview/CommentModal.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Form, Input, Modal, message } from 'antd';
-import { useState } from 'react';
-import { ApiError } from '../../api/client';
-import { postComment } from '../../services/api/documents';
-
-type CommentModalProps = { documentId: string; open: boolean; onClose: () => void };
-
-export default function CommentModal({ documentId, open, onClose }: CommentModalProps) {
- const [comment, setComment] = useState('');
- const [saving, setSaving] = useState(false);
- const close = () => {
- setComment('');
- onClose();
- };
- const handleOk = async () => {
- setSaving(true);
- try {
- await postComment(documentId, comment.trim());
- message.success('Комментарий добавлен');
- close();
- } catch (e: unknown) {
- // окно не закрываем: текст не теряется, можно повторить
- message.error(e instanceof ApiError ? e.message : 'Не удалось добавить комментарий');
- } finally {
- setSaving(false);
- }
- };
- return (
-
- Комментарий}>
- setComment(e.target.value)}
- autoSize={{ minRows: 3 }}
- maxLength={4000}
- placeholder='Опишите результат проверки или внесенные правки'
- />
-
-
-
- );
-}