Склонять «согласование» → «исполнение», когда согласующих в маршруте нет

Три общих (не привязанных к одной форме) места хардкодили лексику
согласования независимо от того, есть ли реально фаза согласования:

- initialSubmitStatus (shared/approval) — статус заявки при ОТПРАВКЕ
  считался «На согласовании» безусловно, даже когда маршрут состоит
  только из исполнителей (0 согласующих). Теперь статус зависит от
  количества шагов каждой фазы: 0 согласующих + есть исполнители →
  сразу «Выполнение»; ни тех, ни других → «Согласовано».
- ApprovalMatrix — подпись «Доп. согласующий, добавил:» у переназначенного
  участника была захардкожена; теперь выводится из roleLabel, поэтому у
  блока «Исполнение» (roleLabel="Исполнитель") корректно пишет «Доп.
  исполнитель».
- DecisionPanel — сообщения после переназначения («Согласующий добавлен
  перед вами…», «сейчас рассматривает…») не различали фазу шага; теперь
  склоняются по isExecution.

В ReferralForm — единственной форме, где маршрут реально может быть без
согласующих (только исполнитель) — использованы initialSubmitStatus и
локальный noApprovalPhase (matrix.rows.length === 0, подтверждено загрузкой)
для подписи кнопки отправки и текста успеха. Уведомление активного шага
теперь срабатывает и при статусе «Выполнение», не только «На согласовании».

Блок «Маршрут согласования» остаётся видимым и пустым, даже когда шагов
нет — матрица редактируема, заявитель может дописать согласующего вручную.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-08-21 17:00:05 +05:00
parent a0d2907cc0
commit 0cf8359a75
5 changed files with 73 additions and 19 deletions
@@ -23,7 +23,8 @@ import {
IApproverToWrite,
unassignedStepMessage,
resolveWriteListId,
APPROVAL_PHASE
APPROVAL_PHASE,
initialSubmitStatus
} from '../../shared/approval';
import { IRequestFormProps } from '../../models/types';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -253,10 +254,11 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
if (onSaved) {
onSaved();
}
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
// не текущей: поток ищет строку маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval) {
// Отправка (на согласование ИЛИ, если согласующих нет, сразу на исполнение) —
// уведомляем первый активный шаг (поток оповещения). Имена списков — той же
// партиции, куда фактически записана заявка (writeListId), не текущей:
// поток ищет строку маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution) {
await notifyActiveStep(context, approval, id, {
requestTypeId: rtId,
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
@@ -286,8 +288,21 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
return undefined;
};
// Маршрута может не быть вообще (нет согласующих — только исполнитель, как у
// рефералки): initialSubmitStatus сразу пишет «Выполнение», минуя «На
// согласовании», которого в таком маршруте попросту не бывает.
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
const handleSubmit = (): Promise<void> =>
submit.run(
'submit',
save(initialSubmitStatus(matrix.rows.length, executors.rows.length)),
validateSubmit
);
// Согласующих в маршруте нет вообще (подтверждено — матрица загружена без
// ошибки и пуста), а не просто «ещё загружается»: до этого момента считаем,
// что фаза согласования есть — исторический дефолт, без мигания подписи.
const noApprovalPhase = !matrix.loading && !matrix.error && matrix.rows.length === 0;
const sectionProps = { data, set, context, onReferrerChange: handleReferrerChange };
@@ -318,7 +333,11 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
>
{submit.savedKind && !submit.error && (
<Alert variant="ok">
{submit.savedKind === 'draft' ? 'Черновик сохранён.' : 'Заявка отправлена на согласование.'}
{submit.savedKind === 'draft'
? 'Черновик сохранён.'
: noApprovalPhase
? 'Заявка отправлена на исполнение.'
: 'Заявка отправлена на согласование.'}
</Alert>
)}
{submit.error && <Alert variant="err">{submit.error}</Alert>}
@@ -331,13 +350,16 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
<AssessmentSection {...sectionProps} />
</FormModeProvider>
{/* Блок остаётся видимым и пустым (не скрываем по noApprovalPhase) —
маршрут без согласующих не значит, что их нельзя добавить: матрица
редактируема, заявитель вправе вписать согласующего сам. */}
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
{!executors.loading && executors.rows.length > 0 && (
<ApprovalMatrix
editor={executors}
search={(q) => approval.searchUsers(q)}
title="Исполнение (после согласования)"
title={noApprovalPhase ? 'Исполнение' : 'Исполнение (после согласования)'}
roleLabel="Исполнитель"
emptyText="Исполнители не заданы по маршруту."
/>
@@ -350,7 +372,7 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
onCancel={onCancel}
onSaveDraft={handleSaveDraft}
onSubmit={handleSubmit}
submitLabel="Отправить на согласование"
submitLabel={noApprovalPhase ? 'Отправить на исполнение' : 'Отправить на согласование'}
/>
</FormCard>
</div>
@@ -7,6 +7,7 @@ export {
APPROVAL_PHASE_CHOICES,
EDITABLE_STATUSES,
isEditableStatus,
initialSubmitStatus,
} from './models/statuses';
export type { ISiteUser } from './models/ISiteUser';
export type { IMatrixRow } from './models/IMatrixRow';
@@ -43,6 +43,24 @@ export function isEditableStatus(status: string | undefined): boolean {
return !status || EDITABLE_STATUSES.indexOf(status) !== -1;
}
/**
* Статус ЗАЯВКИ в момент ОТПРАВКИ — до записи строк маршрута (см. writeRoute),
* когда реальных строк ещё нет и решать по ним, как computeRequestStatus в
* approverDecision.service.ts, нельзя. Формы раньше писали «На согласовании»
* безусловно; это неверно для маршрута без согласующих (только исполнение,
* см. форму «Реферальная рекомендация») — там заявка должна сразу войти в
* фазу исполнения, минуя несуществующую фазу согласования.
*
* Порядок фаз — тот же, что у computeRequestStatus (согласование → исполнение
* → закрыта), только по КОЛИЧЕСТВУ шагов, а не по фактическим статусам строк
* (при отправке все строки ещё «Ждёт очереди»/не существуют).
*/
export function initialSubmitStatus(approversCount: number, executorsCount: number): string {
if (approversCount > 0) return REQUEST_STATUS.onApproval;
if (executorsCount > 0) return REQUEST_STATUS.execution;
return REQUEST_STATUS.approved;
}
// Наборов значений ДВА, и они не совпадают — это разные сущности:
//
// 1) статус ЗАЯВКИ — колонка RequestStatus в списке «… — данные». Пишется
@@ -42,6 +42,11 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
}) => {
const { rows, loading, error, editable, blocks } = editor;
// Подпись «доп. согласующий»/«доп. исполнитель» — из roleLabel, а не
// хардкод: иначе блок «Исполнение» (roleLabel="Исполнитель") показывал бы
// добавленному через переназначение исполнителю подпись «Доп. согласующий».
const addedByLabel = `Доп. ${roleLabel.charAt(0).toLowerCase()}${roleLabel.slice(1)}, добавил:`;
// Ключи строк и анимация их перестановки. Ключи обязаны быть устойчивыми
// (не индекс) — иначе React переписывает строки на месте, и «переезжать»
// при перестановке шага будет нечему (см. matrixRowKeys / useFlipRows).
@@ -155,13 +160,13 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
{!r.who && r.warning && (
<span className={styles.warningInline}>{r.warning}</span>
)}
{/* Доп. согласующий, добавленный другим согласующим (с правом
переназначения) — помечаем, кто его добавил. Пусто у обычных
строк маршрута. Как и комментарий — inline под именем, чтобы
не ломать rowSpan колонки «Шаг» у параллельных шагов. */}
{/* Доп. участник шага, добавленный переназначением — помечаем,
кто его добавил. Пусто у обычных строк маршрута. Как и
комментарий — inline под именем, чтобы не ломать rowSpan
колонки «Шаг» у параллельных шагов. */}
{r.addedBy && (
<span className={styles.addedByInline}>
<span className={styles.addedByLabel}>Доп. согласующий, добавил:</span>{' '}
<span className={styles.addedByLabel}>{addedByLabel}</span>{' '}
{r.addedBy}
</span>
)}
@@ -142,12 +142,18 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({
}
if (delegated > 0) {
// Склоняем по фазе шага: у исполнения нет «согласующих» — тот, кого
// добавили, реально выполняет заявку, а не рассматривает её.
const who = isExecution ? 'Исполнитель' : 'Согласующий';
const whoMany = isExecution ? 'Исполнителей' : 'Согласующих';
const verb = isExecution ? 'выполнит' : 'рассмотрит';
const verbMany = isExecution ? 'выполнят' : 'рассмотрят';
return (
<div className={styles.panel}>
<Alert variant="ok">
{delegated === 1
? 'Согласующий добавлен перед вами — он рассмотрит заявку раньше вас. Ваш шаг остаётся в маршруте.'
: `Согласующих добавлено: ${delegated}. Они рассмотрят заявку раньше вас, в порядке добавления. ` +
? `${who} добавлен перед вами — он ${verb} заявку раньше вас. Ваш шаг остаётся в маршруте.`
: `${whoMany} добавлено: ${delegated}. Они ${verbMany} заявку раньше вас, в порядке добавления. ` +
'Ваш шаг остаётся в маршруте.'}
</Alert>
</div>
@@ -234,9 +240,11 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({
<div className={styles.panel}>
<div className={styles.title}>{isExecution ? 'Исполнение' : 'Ваше решение'}</div>
<Alert variant="warn">
Заявку сейчас рассматривает сотрудник, которого вы добавили перед собой. Ваш шаг никуда
не делся — он вернётся к вам, когда очередь дойдёт. Если добавленный сотрудник
недоступен, можно добавить ещё.
{isExecution
? 'Заявку сейчас выполняет сотрудник, которого вы добавили перед собой.'
: 'Заявку сейчас рассматривает сотрудник, которого вы добавили перед собой.'}{' '}
Ваш шаг никуда не делся — он вернётся к вам, когда очередь дойдёт. Если добавленный
сотрудник недоступен, можно добавить ещё.
</Alert>
{error && <Alert variant="err">{error}</Alert>}
{delegateBlock}