diff --git a/src/webparts/requestPortal/features/myRequests/RequestsTableView.module.scss b/src/webparts/requestPortal/features/myRequests/RequestsTableView.module.scss
index 07b4978..411ad0e 100644
--- a/src/webparts/requestPortal/features/myRequests/RequestsTableView.module.scss
+++ b/src/webparts/requestPortal/features/myRequests/RequestsTableView.module.scss
@@ -198,6 +198,18 @@
width: 190px;
}
+// Причина возврата — свободный текст согласующего, поэтому переносим по словам
+// (в т.ч. длинное слово без пробелов, иначе оно растянет таблицу).
+.colComment {
+ white-space: normal;
+ overflow-wrap: anywhere;
+ color: $gray-700;
+}
+
+.commentEmpty {
+ color: $gray-400;
+}
+
// Раскраска статуса — теперь только в shared/approval/ui/StatusBadge (см.
// использование ниже с size="sm"). Раньше здесь была своя копия этих цветов —
// она разошлась с эталоном (На согласовании/Возвращена на доработку оказались
diff --git a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx
index 37c67e5..16b439d 100644
--- a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx
+++ b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx
@@ -9,6 +9,7 @@ import {
getOwnRequests,
getOwnRequestsByStatus,
hydratePendingAuthors,
+ hydrateReturnComments,
searchOwnRequests,
} from './myRequests.service';
import { IMyRequest } from './models/IMyRequest';
@@ -115,11 +116,27 @@ function statusOptionClassName(status: string): string {
// 'own:created' — статус нужен, только пока показаны ВСЕ заявки. Стоит выбрать
// его в фильтре, и колонка вырождается в тот же столбец одинаковых плашек,
// поэтому она зависит не от одного вида, а ещё и от фильтра.
-function columnsFor(view: ListView, statusFilter: string): { author: boolean; status: boolean } {
- if (view === 'approvals' || view === 'execution') return { author: true, status: false };
- if (view === 'rework') return { author: false, status: false };
- if (view === 'own:created') return { author: false, status: !statusFilter };
- return { author: true, status: true };
+//
+// «Причина возврата» — там, где показаны ТОЛЬКО возвращённые заявки: для них
+// это главное, ради чего в список и заходят (что именно просят исправить).
+interface ITableColumns {
+ author: boolean;
+ status: boolean;
+ returnComment: boolean;
+}
+
+function columnsFor(view: ListView, statusFilter: string): ITableColumns {
+ const returnedOnly =
+ view === 'rework' || (view === 'own:created' && statusFilter === REQUEST_STATUS.returned);
+
+ if (view === 'approvals' || view === 'execution') {
+ return { author: true, status: false, returnComment: false };
+ }
+ if (view === 'rework') return { author: false, status: false, returnComment: true };
+ if (view === 'own:created') {
+ return { author: false, status: !statusFilter, returnComment: returnedOnly };
+ }
+ return { author: true, status: true, returnComment: false };
}
export interface IRequestsTableViewProps {
@@ -171,6 +188,7 @@ const Table: React.FC<{
Дата |
{columns.author && Автор | }
{columns.status && Статус | }
+ {columns.returnComment && Причина возврата | }
@@ -203,6 +221,11 @@ const Table: React.FC<{
{r.status ? : '—'}
)}
+ {columns.returnComment && (
+
+ {r.returnComment || Без комментария}
+ |
+ )}
))}
@@ -323,7 +346,13 @@ export const RequestsTableView: React.FC = ({
// зашитым в раздел, а не выбранным в фильтре.
if (view === 'rework' || (view === 'own:created' && statusFilter)) {
const status = view === 'rework' ? REQUEST_STATUS.returned : statusFilter;
- return getOwnRequestsByStatus(context, types, status, page, debouncedQuery);
+ const result = await getOwnRequestsByStatus(context, types, status, page, debouncedQuery);
+ // Причина возврата живёт в строке маршрута — дочитываем только для
+ // видимой страницы и только там, где колонка показывается.
+ if (status === REQUEST_STATUS.returned) {
+ await hydrateReturnComments(context, types, result.rows);
+ }
+ return result;
}
if (view === 'own:created') {
diff --git a/src/webparts/requestPortal/features/myRequests/models/IMyRequest.ts b/src/webparts/requestPortal/features/myRequests/models/IMyRequest.ts
index 4630ccc..b07f51d 100644
--- a/src/webparts/requestPortal/features/myRequests/models/IMyRequest.ts
+++ b/src/webparts/requestPortal/features/myRequests/models/IMyRequest.ts
@@ -22,4 +22,10 @@ export interface IMyRequest {
* не показывается — поэтому и не входит в pageSignature.
*/
myStepStatus?: string;
+ /**
+ * Комментарий согласующего, вернувшего заявку на доработку. Живёт не в списке
+ * данных, а в строке МАРШРУТА, поэтому дочитывается отдельно и только там,
+ * где нужен (см. hydrateReturnComments).
+ */
+ returnComment?: string;
}
diff --git a/src/webparts/requestPortal/features/myRequests/myRequests.service.ts b/src/webparts/requestPortal/features/myRequests/myRequests.service.ts
index 47d4edf..09ebc51 100644
--- a/src/webparts/requestPortal/features/myRequests/myRequests.service.ts
+++ b/src/webparts/requestPortal/features/myRequests/myRequests.service.ts
@@ -785,6 +785,93 @@ export async function getOwnRequestsByStatus(
};
}
+// Список согласования по типу заявки: Title списка данных → Title списка
+// согласования по конвенции имён (та же логика, что в indexApprovalsForType).
+// undefined — список заведён не по конвенции, маршрута для него не найти.
+async function approvalListFor(
+ context: WebPartContext,
+ type: IRequestType
+): Promise {
+ const sp = spForSite(type.siteUrl as string, context);
+ const info = (await sp.web.getList(type.listId as string).select('Title')()) as { Title?: string };
+ const dataTitle = info.Title || '';
+ if (!DATA_LIST_SUFFIX.test(dataTitle)) return undefined;
+ return sp.web.lists.getByTitle(`${dataTitle.replace(DATA_LIST_SUFFIX, '')} — согласование`);
+}
+
+// Сколько строк маршрута максимум забираем за раз при дочитывании комментариев:
+// на странице ≤20 заявок, у каждой обычно единицы шагов — запас многократный.
+const RETURN_COMMENT_ROW_LIMIT = 2000;
+
+/**
+ * Проставляет returnComment — комментарий согласующего, вернувшего заявку на
+ * доработку. Причина возврата хранится в СТРОКЕ МАРШРУТА (колонка Comment
+ * списка «— согласование»), а не в самой заявке, поэтому её нельзя получить
+ * тем же запросом, что и список: нужен отдельный проход по спискам согласования.
+ *
+ * Вызывается ТОЛЬКО там, где причина показывается (см. RequestsTableView) —
+ * счётчику в меню, например, она не нужна, и лишние запросы ему ни к чему.
+ * Мутирует переданные строки — как и hydrateAuthors рядом.
+ */
+export async function hydrateReturnComments(
+ context: WebPartContext,
+ types: IRequestType[],
+ rows: IMyRequest[]
+): Promise {
+ if (rows.length === 0) return;
+
+ const typeById = new Map();
+ types.forEach((t) => typeById.set(t.id, t));
+
+ const rowsByType = new Map();
+ rows.forEach((r) => {
+ const list = rowsByType.get(r.requestTypeId) || [];
+ list.push(r);
+ rowsByType.set(r.requestTypeId, list);
+ });
+
+ await Promise.all(
+ Array.from(rowsByType.entries()).map(async ([requestTypeId, typeRows]) => {
+ const type = typeById.get(requestTypeId);
+ if (!type) return;
+ try {
+ const approvalList = await approvalListFor(context, type);
+ if (!approvalList) return;
+
+ // Фильтруем только по TicketId (числовое поле), статус сравниваем в JS:
+ // кириллица в $filter по Choice-колонке молча возвращает 0 строк —
+ // ровно та же причина, что и в indexApprovalsForType.
+ const stepRows = (await approvalList.items
+ .filter(typeRows.map((r) => `TicketId eq ${r.id}`).join(' or '))
+ .select('TicketId', 'ApprovalStatus', 'Comment')
+ .top(RETURN_COMMENT_ROW_LIMIT)()) as Array>;
+
+ const commentByTicket = new Map();
+ stepRows.forEach((r) => {
+ if (r.ApprovalStatus !== REQUEST_STATUS.returned) return;
+ const ticketId = r.TicketId as number | undefined;
+ const comment = ((r.Comment as string) || '').trim();
+ // Возврат по заявке один (при переотправке маршрут переписывается
+ // целиком, см. writeRoute) — но если строк вдруг несколько, берём
+ // первую непустую, а не последнюю пустую.
+ if (ticketId && comment && !commentByTicket.has(ticketId)) {
+ commentByTicket.set(ticketId, comment);
+ }
+ });
+
+ typeRows.forEach((r) => {
+ r.returnComment = commentByTicket.get(r.id);
+ });
+ } catch (e) {
+ console.error(
+ `[Текущие заявки] «${type.name}»: не удалось прочитать причину возврата:`,
+ (e as Error).message
+ );
+ }
+ })
+ );
+}
+
// «На согласовании»: список заявок уже отобран по строкам согласования
// (getPendingApprovalIndex — там же определяется, чья сейчас очередь,
// обычным REST). Здесь среди НИХ фильтруем по тексту — только через Search
diff --git a/src/webparts/requestPortal/features/myRequests/pageCache.test.ts b/src/webparts/requestPortal/features/myRequests/pageCache.test.ts
index 729e595..153ad1c 100644
--- a/src/webparts/requestPortal/features/myRequests/pageCache.test.ts
+++ b/src/webparts/requestPortal/features/myRequests/pageCache.test.ts
@@ -99,6 +99,18 @@ describe('pageSignature', () => {
expect(pageSignature(before)).toBe(pageSignature(after));
});
+ it('изменение причины возврата меняет подпись — она видна в таблице «Доработка»', () => {
+ const before = page({ rows: [request({ returnComment: 'Исправьте должность' })] });
+ const after = page({ rows: [request({ returnComment: 'Приложите скан' })] });
+ expect(pageSignature(before)).not.toBe(pageSignature(after));
+ });
+
+ it('появление причины возврата у строки без неё тоже меняет подпись', () => {
+ const before = page({ rows: [request()] });
+ const after = page({ rows: [request({ returnComment: 'Исправьте должность' })] });
+ expect(pageSignature(before)).not.toBe(pageSignature(after));
+ });
+
it('пустая страница имеет стабильную подпись', () => {
expect(pageSignature({ rows: [], totalRows: 0 })).toBe(pageSignature({ rows: [], totalRows: 0 }));
});
diff --git a/src/webparts/requestPortal/features/myRequests/pageCache.ts b/src/webparts/requestPortal/features/myRequests/pageCache.ts
index 8159322..52d1569 100644
--- a/src/webparts/requestPortal/features/myRequests/pageCache.ts
+++ b/src/webparts/requestPortal/features/myRequests/pageCache.ts
@@ -38,6 +38,8 @@ export function writePage(key: string, page: IPage): void {
// перерисовываясь тем же самым содержимым.
export function pageSignature(page: IPage): string {
return `${page.totalRows}|${page.rows
- .map((r) => [r.requestTypeId, r.id, r.title, r.status, r.created, r.authorName].join('~'))
+ .map((r) =>
+ [r.requestTypeId, r.id, r.title, r.status, r.created, r.authorName, r.returnComment ?? ''].join('~')
+ )
.join(';')}`;
}