feat: столбец «Исполнитель» в таблицах заявок
Показывает, у кого заявка сейчас: ФИО и почта согласующего или исполнителя текущего шага (несколько — параллельный шаг, при делегировании — делегат), при доработке — инициатор. Есть в «Инициированных», «Согласованных», «Выполненных мной» и «Все заявки»; в очередях и «Доработке» держатель всегда сам пользователь. Дочитывается только для видимой страницы, одним запросом на список согласования. Группировка по партициям вынесена в approvalPartitionsFor и общая с комментарием к решению. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
getLinkedTypes,
|
||||
getOwnRequests,
|
||||
getOwnRequestsByStatus,
|
||||
hydrateCurrentAssignees,
|
||||
hydrateDecisionComments,
|
||||
hydratePendingAuthors,
|
||||
searchOwnRequests,
|
||||
@@ -131,11 +132,32 @@ function decisionStatusFor(view: ListView, statusFilter: string): string | undef
|
||||
interface ITableColumns {
|
||||
author: boolean;
|
||||
status: boolean;
|
||||
/** Колонка «Исполнитель» — у кого заявка сейчас (см. showsAssignee). */
|
||||
assignee: boolean;
|
||||
/** Заголовок колонки с комментарием; undefined — колонки нет. */
|
||||
commentLabel?: string;
|
||||
}
|
||||
|
||||
// «Исполнитель» нужен там, где заявка может быть у кого угодно: свои поданные
|
||||
// заявки, история своих решений (заявка ушла дальше по маршруту) и «Все
|
||||
// заявки». В очередях согласования/исполнения и в «Доработке» держатель —
|
||||
// всегда сам пользователь, в «Отклонённых мной» — никто: колонка вышла бы из
|
||||
// одинаковых значений.
|
||||
//
|
||||
// На «Инициированных» с фильтром по статусу — только если при этом статусе у
|
||||
// заявки вообще бывает держатель по маршруту. При «Возвращена на доработку»
|
||||
// это всегда сам автор, при итоговых статусах — никто.
|
||||
const ASSIGNEE_VIEWS: ListView[] = ['own:created', 'own:approved', 'own:executed', 'all'];
|
||||
const STATUSES_WITH_ROUTE_HOLDER: string[] = [REQUEST_STATUS.onApproval, REQUEST_STATUS.execution];
|
||||
|
||||
function showsAssignee(view: ListView, statusFilter: string): boolean {
|
||||
if (ASSIGNEE_VIEWS.indexOf(view) < 0) return false;
|
||||
if (view === 'own:created' && statusFilter) return STATUSES_WITH_ROUTE_HOLDER.indexOf(statusFilter) >= 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
function columnsFor(view: ListView, statusFilter: string): ITableColumns {
|
||||
const assignee = showsAssignee(view, statusFilter);
|
||||
const decisionStatus = decisionStatusFor(view, statusFilter);
|
||||
const commentLabel =
|
||||
decisionStatus === REQUEST_STATUS.rejected
|
||||
@@ -144,15 +166,15 @@ function columnsFor(view: ListView, statusFilter: string): ITableColumns {
|
||||
? 'Причина возврата'
|
||||
: undefined;
|
||||
|
||||
if (view === 'approvals' || view === 'execution') return { author: true, status: false };
|
||||
if (view === 'rework') return { author: false, status: false, commentLabel };
|
||||
if (view === 'own:created') return { author: false, status: !statusFilter, commentLabel };
|
||||
if (view === 'approvals' || view === 'execution') return { author: true, status: false, assignee };
|
||||
if (view === 'rework') return { author: false, status: false, assignee, commentLabel };
|
||||
if (view === 'own:created') return { author: false, status: !statusFilter, assignee, commentLabel };
|
||||
// 'all' — заявки всех авторов вперемешку, поэтому нужны и автор, и статус.
|
||||
if (view === 'all') return { author: true, status: true };
|
||||
if (view === 'all') return { author: true, status: true, assignee };
|
||||
// История решений: заявка чужая, поэтому нужен автор. Статус — только если он
|
||||
// не предопределён самой вкладкой («Отклонённые мной» — это всегда «Отклонено»,
|
||||
// а вот у согласованных/выполненных заявка могла уйти дальше по маршруту).
|
||||
return { author: true, status: !decisionStatus, commentLabel };
|
||||
return { author: true, status: !decisionStatus, assignee, commentLabel };
|
||||
}
|
||||
|
||||
export interface IRequestsTableViewProps {
|
||||
@@ -189,6 +211,11 @@ function formatDate(iso: string): string {
|
||||
return `${dd}.${mm}.${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
/** Человек в ячейке — «ФИО · почта»; колонки «Автор» и «Исполнитель». */
|
||||
function personLabel(name: string, email: string): string {
|
||||
return [name, email].filter(Boolean).join(' · ') || '—';
|
||||
}
|
||||
|
||||
// Таблица заявок. Клик в ЛЮБОМ месте строки открывает заявку (кликабельна вся
|
||||
// строка, а не только название), поэтому строка — <tr> с onClick + tabIndex
|
||||
// (доступ с клавиатуры: Enter/Space).
|
||||
@@ -236,7 +263,7 @@ const Table: React.FC<{
|
||||
key: 'author',
|
||||
title: 'Автор',
|
||||
width: '280px',
|
||||
render: (r) => [r.authorName, r.authorEmail].filter(Boolean).join(' · ') || '—'
|
||||
render: (r) => personLabel(r.authorName, r.authorEmail)
|
||||
});
|
||||
}
|
||||
if (cols.status) {
|
||||
@@ -247,6 +274,22 @@ const Table: React.FC<{
|
||||
render: (r) => (r.status ? <StatusBadge status={r.status} size="sm" /> : '—')
|
||||
});
|
||||
}
|
||||
if (cols.assignee) {
|
||||
columns.push({
|
||||
key: 'assignee',
|
||||
title: 'Исполнитель',
|
||||
// Та же ширина, что у «Автора»: содержимое того же вида.
|
||||
width: '280px',
|
||||
// «ФИО · почта», как в колонке «Автор». Параллельный шаг — каждый
|
||||
// человек отдельной строкой.
|
||||
render: (r) =>
|
||||
r.currentAssignees && r.currentAssignees.length > 0
|
||||
? r.currentAssignees.map((a) => (
|
||||
<div key={`${a.email}|${a.name}`}>{personLabel(a.name, a.email)}</div>
|
||||
))
|
||||
: '—'
|
||||
});
|
||||
}
|
||||
if (cols.commentLabel) {
|
||||
columns.push({
|
||||
key: 'comment',
|
||||
@@ -522,9 +565,11 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
||||
const fetchPage = async (onPartial?: (p: IPage) => void, reuseFull = false): Promise<IPage> => {
|
||||
const result = await fetchRows(onPartial, reuseFull);
|
||||
const decisionStatus = decisionStatusFor(view, statusFilter);
|
||||
if (decisionStatus) {
|
||||
await hydrateDecisionComments(context, types, result.rows, decisionStatus);
|
||||
}
|
||||
// Оба дочитывания из строк маршрута независимы — параллельно.
|
||||
await Promise.all([
|
||||
decisionStatus ? hydrateDecisionComments(context, types, result.rows, decisionStatus) : undefined,
|
||||
showsAssignee(view, statusFilter) ? hydrateCurrentAssignees(context, types, result.rows) : undefined,
|
||||
]);
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user