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