diff --git a/frontend/src/app/requests/page.tsx b/frontend/src/app/requests/page.tsx index 4a00943c..670b912d 100644 --- a/frontend/src/app/requests/page.tsx +++ b/frontend/src/app/requests/page.tsx @@ -140,19 +140,25 @@ function RequestsPageContent() { { screen.closeDetailsRequest(); h.openEdit(request); }} onPreviewAttachment={h.setAttachmentPreview} onReject={h.handleReject} + onSetCommentDraft={h.setCommentDraft} request={h.detailsRequest} /> diff --git a/frontend/src/components/requests/RequestDetailModal.tsx b/frontend/src/components/requests/RequestDetailModal.tsx index 445205b8..5a7a3650 100644 --- a/frontend/src/components/requests/RequestDetailModal.tsx +++ b/frontend/src/components/requests/RequestDetailModal.tsx @@ -5,41 +5,54 @@ import { statusMeta, type RequestAttachmentPreview, } from "@/hooks/useRequestsPage"; -import { formatDate, formatDateTime } from "@/lib/shared"; -import type { Request } from "@/types/api"; +import { displayUserName, formatDate, formatDateTime } from "@/lib/shared"; +import type { Request, RequestComment } from "@/types/api"; import { Ban, Paperclip, Pencil, ThumbsDown, ThumbsUp, Trash2 } from "lucide-react"; +import { CommentComposer, CommentDeleteButton } from "@/components/shared/CommentControls"; import { getRequestActionState } from "./requestActions"; import { RequestUserBadge } from "./RequestUserBadge"; type RequestDetailModalProps = { actionError?: string | null; busyKey: string | null; + commentDraft: string; + comments: RequestComment[]; + commentsLoading: boolean; currentUserId?: number | null; departmentNameMap: Map; isFinal: (status?: string) => boolean; + onAddComment: (requestId: number) => void | Promise; onApprove: (requestId: number) => void | Promise; onCancel: (requestId: number) => void | Promise; onClose: () => void; onDelete: (requestId: number) => void | Promise; + onDeleteComment: (requestId: number, commentId: number) => void | Promise; onEdit: (request: Request) => void; onPreviewAttachment: (preview: RequestAttachmentPreview) => void; onReject: (requestId: number) => void | Promise; + onSetCommentDraft: (requestId: number, value: string) => void; request: Request | null; }; export function RequestDetailModal({ actionError, busyKey, + commentDraft, + comments, + commentsLoading, currentUserId, departmentNameMap, isFinal, + onAddComment, onApprove, onCancel, onClose, onDelete, + onDeleteComment, onEdit, onPreviewAttachment, onReject, + onSetCommentDraft, request, }: RequestDetailModalProps) { if (!request) return null; @@ -47,7 +60,7 @@ export function RequestDetailModal({ const author = request.employee || request.created_by; const approver = request.approver || request.assigned_to; const actionState = getRequestActionState(request, currentUserId, isFinal); - const { canCancel, canDelete, canEdit, canProcess } = actionState; + const { canCancel, canComment, canDelete, canEdit, canProcess } = actionState; const status = statusMeta[String(request.status || "").toLowerCase()] ?? defaultStatusMeta; const typeLabel = requestTypeLabels[String(request.type || request.request_type || "").toLowerCase()] || String(request.type || request.request_type || "Другое"); @@ -55,6 +68,7 @@ export function RequestDetailModal({ const attachmentName = attachmentUrl ? decodeURIComponent(attachmentUrl.split("/").pop() || "Вложение") : ""; + const commentCount = request.comments_count ?? comments.length; return (
-
-
-
- - {typeLabel} - - - {status.label} - -
-

- {request.display_title || request.title || "Без заголовка"} -

- {(request.comment || request.description) ? ( -
- {request.comment || request.description} +
+
+
+
+ + {typeLabel} + + + {status.label} +
- ) : ( -

Описание отсутствует

- )} +

+ {request.display_title || request.title || "Без заголовка"} +

+ {(request.comment || request.description) ? ( +
+ {request.comment || request.description} +
+ ) : ( +

Описание отсутствует

+ )} +
+
+ +
+
+
+

Комментарии

+ + {commentCount} + +
+ + {!canComment ? ( +

+ Комментарии будут доступны после отправки заявления. +

+ ) : ( +
+
+ {commentsLoading ? ( +
+
+ Загрузка комментариев... +
+ ) : comments.length > 0 ? ( + comments.map((comment) => ( +
+
+
+

+ {displayUserName(comment.author)} +

+

+ {formatDateTime(comment.created_at) || formatDate(comment.created_at)} +

+
+ {Boolean(comment.author?.id && currentUserId === comment.author.id) ? ( + onDeleteComment(request.id, comment.id)} + /> + ) : null} +
+

+ {comment.text} +

+
+ )) + ) : ( +

Комментариев пока нет

+ )} +
+ + onSetCommentDraft(request.id, value)} + onSubmit={() => onAddComment(request.id)} + placeholder="Добавить комментарий" + /> +
+ )} +
-
-
+
+

Даты

@@ -180,7 +261,7 @@ export function RequestDetailModal({
-
+

Участники

@@ -207,81 +288,85 @@ export function RequestDetailModal({
-
-

Отделы

- {(request.departments || []).length > 0 ? ( -
- {(request.departments || []).map((id) => ( - - {departmentNameMap.get(Number(id)) || `Отдел #${id}`} - - ))} -
- ) : ( -

Отделы не указаны

- )} +
+
+

Отделы

+ {(request.departments || []).length > 0 ? ( +
+ {(request.departments || []).map((id) => ( + + {departmentNameMap.get(Number(id)) || `Отдел #${id}`} + + ))} +
+ ) : ( +

Отделы не указаны

+ )} +
+ +
+

Получатели

+ {(request.recipients || []).length > 0 ? ( +
+ {(request.recipients || []).map((recipient) => ( + + ))} +
+ ) : ( +

Получатели не указаны

+ )} +
-
-

Получатели

- {(request.recipients || []).length > 0 ? ( -
- {(request.recipients || []).map((recipient) => ( - - ))} -
- ) : ( -

Получатели не указаны

- )} -
+
+
+

В копии

+ {(request.cc_users || []).length > 0 ? ( +
+ {(request.cc_users || []).map((ccUser) => ( + + ))} +
+ ) : ( +

Копия не указана

+ )} +
-
-

В копии

- {(request.cc_users || []).length > 0 ? ( -
- {(request.cc_users || []).map((ccUser) => ( - - ))} -
- ) : ( -

Копия не указана

- )} -
- -
-

Вложение

- {attachmentUrl ? ( -
- - - Открыть в новой вкладке - -
- ) : ( -

Вложение отсутствует

- )} +
+

Вложение

+ {attachmentUrl ? ( +
+ + + Открыть в новой вкладке + +
+ ) : ( +

Вложение отсутствует

+ )} +
diff --git a/frontend/src/hooks/useRequestsPage.ts b/frontend/src/hooks/useRequestsPage.ts index aa061ebf..601e79c6 100644 --- a/frontend/src/hooks/useRequestsPage.ts +++ b/frontend/src/hooks/useRequestsPage.ts @@ -109,6 +109,7 @@ export function useRequestsPage(_userId: number | null | undefined) { const [employees, setEmployees] = useState([]); const [departments, setDepartments] = useState([]); const [commentsMap, setCommentsMap] = useState>({}); + const [commentsLoadingMap, setCommentsLoadingMap] = useState>({}); const [expandedRows, setExpandedRows] = useState>({}); const [expandedComments, setExpandedComments] = useState>({}); const [commentDrafts, setCommentDrafts] = useState>({}); @@ -427,12 +428,25 @@ export function useRequestsPage(_userId: number | null | undefined) { }; /* ── comments ── */ + const ensureCommentsLoaded = useCallback(async (requestId: number) => { + if (commentsMap[requestId] || commentsLoadingMap[requestId]) return; + + setCommentsLoadingMap((prev) => ({ ...prev, [requestId]: true })); + try { + const comments = await apiClient.getRequestComments(requestId); + setCommentsMap((prev) => ({ ...prev, [requestId]: comments })); + } catch { + setCommentsMap((prev) => ({ ...prev, [requestId]: [] })); + } finally { + setCommentsLoadingMap((prev) => ({ ...prev, [requestId]: false })); + } + }, [commentsLoadingMap, commentsMap]); + const toggleComments = async (requestId: number) => { const isOpen = Boolean(expandedComments[requestId]); setExpandedComments((p) => ({ ...p, [requestId]: !isOpen })); if (!isOpen && !commentsMap[requestId]) { - try { const c = await apiClient.getRequestComments(requestId); setCommentsMap((p) => ({ ...p, [requestId]: c })); } - catch { setCommentsMap((p) => ({ ...p, [requestId]: [] })); } + await ensureCommentsLoaded(requestId); } }; @@ -477,6 +491,13 @@ export function useRequestsPage(_userId: number | null | undefined) { return () => { if (el) observer.unobserve(el); }; }, [nextPage, loadingMore, loading, handleLoadMore]); + useEffect(() => { + const requestId = detailsRequest?.id; + if (!requestId) return; + if (String(detailsRequest.status || "").toLowerCase() === "draft") return; + void ensureCommentsLoaded(requestId); + }, [detailsRequest, ensureCommentsLoaded]); + return { /* data */ requests: filteredRequests, @@ -485,6 +506,7 @@ export function useRequestsPage(_userId: number | null | undefined) { departments, departmentNameMap, commentsMap, + commentsLoadingMap, expandedRows, expandedComments, commentDrafts,