правки по логике переключения
Tests / frontend-test (push) Successful in 45s
Tests / backend-test (push) Successful in 2m11s

This commit is contained in:
gorbenko.pavel
2026-08-07 16:04:22 +05:00
parent 1d45780294
commit 11738b6e4a
5 changed files with 98 additions and 15 deletions
@@ -1,7 +1,7 @@
import * as React from 'react';
import tableStyles from '../ui/DataTable.module.scss';
import styles from './ManageRequestsScreen.module.scss';
import { Employee, ManagedApprovalRow, isEditableRowStatus } from '../../models';
import { Employee, ManagedApprovalRow, isEditableRowStatus, isMovableRowStatus } from '../../models';
import { texts } from '../../texts';
import { toEmployeeOption } from '../../utils/employee';
import { blockIndexOf, blockRanges, markParallel, withOrdersFrom } from '../../utils/orderBlocks';
@@ -13,6 +13,17 @@ export interface IManageRequestsPhaseTableProps {
rows: ManagedApprovalRow[];
/** Заявка целиком только для просмотра (закрыта) — контролов нет вообще. */
readOnly: boolean;
/**
* Заявка отклонена целиком (REQUEST_STATUS.rejected) — маршрут дальше не
* продолжится ни в одной фазе, поэтому добавлять исполнителя нельзя НИГДЕ,
* даже в фазу, где сейчас вообще нет строк (иначе rows.length===0 не даёт
* phaseClosed сработать по строкам — см. phaseClosed ниже). readOnly здесь
* не подходит: он делает всю заявку read-only целиком (для isFullyClosed —
* REQUEST_STATUS.closed), а отклонённую заявку можно продолжать
* редактировать (переназначать/удалять уже существующие шаги), просто
* новые добавлять больше некуда.
*/
requestRejected: boolean;
statusVariant: (status: string) => BadgeVariant;
onReassign: (row: ManagedApprovalRow) => void;
onRemove: (row: ManagedApprovalRow) => void;
@@ -40,13 +51,20 @@ function diffOrders(original: ManagedApprovalRow[], renumbered: Row[]): Array<{
* повторяет request-portal/shared/approval/ui/ApprovalMatrix.tsx (та же идея:
* колонка «Шаг» объединена по rowSpan на блок, кнопки ↑/↓ двигают блок целиком),
* но с одним отличием: часть строк тут может быть УЖЕ РЕШЕНА (approved/executed/
* rejected/returned) — такие блоки «заблокированы» и никогда не двигаются и не
* участвуют в переключении параллельности. Блокировка распространяется на все
* блоки НЕ ПОЗЖЕ последнего заблокированного — по построению маршрута (шаги
* rejected) ИЛИ возвращена на доработку (returned) — такие блоки «заблокированы»
* и никогда не двигаются и не участвуют в переключении параллельности (см.
* isMovableRowStatus в models/requestManage.ts). Блокировка распространяется на
* все блоки НЕ ПОЗЖЕ последнего заблокированного — по построению маршрута (шаги
* решаются по возрастанию order) заблокированные блоки всегда образуют
* непрерывный префикс, поэтому «после последнего заблокированного» и есть та
* самая граница, ниже которой перестановка запрещена (см. utils/orderBlocks.ts).
*
* Важно: «заблокирован для перестановки» ≠ «нельзя редактировать». Возвращённый
* на доработку шаг двигать нельзя (решение по нему уже принято), но человека на
* нём переназначить всё ещё можно — это две разные проверки (isMovableRowStatus
* для перестановки/параллельности, isEditableRowStatus для переназначения и
* удаления, см. rowEditable ниже).
*
* Переназначение/удаление СТРОКИ (не блока) разрешено независимо от соседей —
* если у одного из двух параллельных согласующих решение уже принято, а у
* другого нет, второго всё ещё можно переназначить.
@@ -55,6 +73,7 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
title,
rows,
readOnly,
requestRejected,
statusVariant,
onReassign,
onRemove,
@@ -74,8 +93,11 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
const withParallel = React.useMemo(() => markParallel(rows) as Row[], [rows]);
const blocks = React.useMemo(() => blockRanges(withParallel), [withParallel]);
// isMovableRowStatus, а НЕ isEditableRowStatus: «Возвращена на доработку» —
// редактируемый статус (переназначить можно), но НЕ движимый — своё слово по
// шагу уже сказано, менять его место в маршруте нельзя (см. models/requestManage.ts).
const lockedBlock = React.useMemo(
() => blocks.map(b => withParallel.slice(b.start, b.end + 1).some(r => !isEditableRowStatus(r.status))),
() => blocks.map(b => withParallel.slice(b.start, b.end + 1).some(r => !isMovableRowStatus(r.status))),
[blocks, withParallel]
);
// Индекс последнего заблокированного блока — все блоки ПОСЛЕ него движимы,
@@ -98,8 +120,12 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
// в статус «Выполнение») — добавлять в неё ещё один шаг бессмысленно, очередь
// сюда больше не вернётся. Пустую фазу (rows.length===0) это не блокирует —
// так можно завести самый первый шаг фазы, у которой согласующих ещё не было
// вовсе.
const phaseClosed = rows.length > 0 && rows.every(r => !isEditableRowStatus(r.status));
// вовсе. requestRejected — отдельная причина закрытости, которая, В ОТЛИЧИЕ
// от строк выше, обязана блокировать добавление даже в ПУСТУЮ фазу: если
// заявку отклонили на согласовании, до фазы исполнения дело уже не дойдёт,
// хотя строк там могло вообще не быть (см. скрин — «Исполнение» пустая, но
// добавлять туда исполнителя всё равно бессмысленно).
const phaseClosed = requestRejected || (rows.length > 0 && rows.every(r => !isEditableRowStatus(r.status)));
const applyReorder = (mutate: (rows: Row[]) => Row[]): void => {
const renumbered = withOrdersFrom(mutate(withParallel), 0);
@@ -160,7 +186,13 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
const isBlockStart = block.start === i;
const movable = blockIndex > lastLockedBlockIndex;
const rowEditable = isEditableRowStatus(r.status);
const canToggle = rowEditable && i !== firstMovableRowIndex;
// Переключение параллельности — тоже перестановка (меняет order
// относительно соседней строки), поэтому смотрим на isMovableRowStatus,
// а не на rowEditable: у «Возвращена на доработку» кнопка
// объединения должна быть недоступна, хотя переназначить/удалить
// такую строку всё ещё можно (см. ветку ниже, где rowEditable решает,
// показывать ActionsCell целиком или lockedHint).
const canToggle = isMovableRowStatus(r.status) && i !== firstMovableRowIndex;
// Отрицательный Id — призрачная строка (добавлена на клиенте,
// ответ сервера с настоящим Id ещё не пришёл, см.
// ManageRequestsScreen/handleAddApprover). Трогать её нельзя:
@@ -264,7 +296,7 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
(см. onAddApprover в ManageRequestsScreen). */}
{!readOnly && phaseClosed && (
<div className={styles.addRow}>
<CellNote>{texts.manageRequests.phaseClosedHint}</CellNote>
<CellNote>{requestRejected ? texts.manageRequests.requestRejectedHint : texts.manageRequests.phaseClosedHint}</CellNote>
</div>
)}