правки по логике переключения
This commit is contained in:
+41
-9
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -321,6 +321,13 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
// от статуса отдельных строк (см. isFullyClosedRequestStatus).
|
||||
const readOnly = matrix ? isFullyClosedRequestStatus(matrix.request.status) : true;
|
||||
|
||||
// Заявка отклонена — дальше по маршруту дело не пойдёт ни в одной фазе, значит
|
||||
// добавлять исполнителя нельзя нигде, даже в фазу, где ещё вообще нет строк
|
||||
// (readOnly для этого не подходит: он рассчитан только на REQUEST_STATUS.closed
|
||||
// и делает ВСЮ заявку неизменяемой, а отклонённую всё ещё можно редактировать —
|
||||
// переназначать/удалять уже существующие шаги, просто новые уже некуда).
|
||||
const requestRejected = matrix?.request.status === REQUEST_STATUS.rejected;
|
||||
|
||||
// Сортировка по order — ОБЯЗАТЕЛЬНА, а не косметика: ManageRequestsPhaseTable
|
||||
// считает блоки и параллельность по ФИЗИЧЕСКОМУ порядку строк в массиве
|
||||
// (markParallel сравнивает order с предыдущей строкой), а оптимистичные
|
||||
@@ -511,6 +518,7 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
title={texts.manageRequests.approvalPhaseTitle}
|
||||
rows={approvalRows}
|
||||
readOnly={readOnly}
|
||||
requestRejected={requestRejected}
|
||||
statusVariant={statusVariant}
|
||||
onReassign={openReassign}
|
||||
onRemove={askDelete}
|
||||
@@ -524,6 +532,7 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
title={texts.manageRequests.executionPhaseTitle}
|
||||
rows={executionRows}
|
||||
readOnly={readOnly}
|
||||
requestRejected={requestRejected}
|
||||
statusVariant={statusVariant}
|
||||
onReassign={openReassign}
|
||||
onRemove={askDelete}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { REQUEST_STATUS, isEditableRowStatus, isFullyClosedRequestStatus } from './requestManage';
|
||||
import { REQUEST_STATUS, isEditableRowStatus, isFullyClosedRequestStatus, isMovableRowStatus } from './requestManage';
|
||||
|
||||
describe('isEditableRowStatus', () => {
|
||||
it('черновик/в очереди/на согласовании/на исполнении — редактируемые', () => {
|
||||
@@ -23,6 +23,26 @@ describe('isEditableRowStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMovableRowStatus', () => {
|
||||
it('черновик/в очереди/на согласовании/на исполнении — движимые', () => {
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.draft)).toBe(true);
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.queued)).toBe(true);
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.onApproval)).toBe(true);
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.onExecution)).toBe(true);
|
||||
});
|
||||
|
||||
it('"Возвращена на доработку" — редактируемый, но НЕ движимый статус', () => {
|
||||
expect(isEditableRowStatus(REQUEST_STATUS.returned)).toBe(true);
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.returned)).toBe(false);
|
||||
});
|
||||
|
||||
it('согласовано/выполнено/отклонено — тоже не движимые', () => {
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.approved)).toBe(false);
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.executed)).toBe(false);
|
||||
expect(isMovableRowStatus(REQUEST_STATUS.rejected)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFullyClosedRequestStatus', () => {
|
||||
it('true только для статуса "Закрыта"', () => {
|
||||
expect(isFullyClosedRequestStatus(REQUEST_STATUS.closed)).toBe(true);
|
||||
|
||||
@@ -27,11 +27,16 @@ export const APPROVAL_PHASE = {
|
||||
} as const;
|
||||
|
||||
// Строка маршрута в одном из этих статусов ЕЩЁ НЕ решена согласующим/исполнителем
|
||||
// ОКОНЧАТЕЛЬНО — её можно менять (переназначить человека, удалить, переставить,
|
||||
// добавить новую рядом). «Возвращена на доработку» сюда включена нарочно: это не
|
||||
// финальное решение (заявка вернётся на новый круг после доработки), поэтому
|
||||
// согласующего на этом шаге всё ещё можно переназначить. approved/executed/
|
||||
// rejected — финальные решения, трогать нельзя ни при каких условиях.
|
||||
// ОКОНЧАТЕЛЬНО — её можно менять (переназначить человека, удалить, добавить новую
|
||||
// рядом). «Возвращена на доработку» сюда включена нарочно: это не финальное
|
||||
// решение (заявка вернётся на новый круг после доработки), поэтому согласующего
|
||||
// на этом шаге всё ещё можно переназначить. approved/executed/rejected —
|
||||
// финальные решения, трогать нельзя ни при каких условиях.
|
||||
//
|
||||
// ВАЖНО: это НЕ то же самое, что «можно переставить» (см. isMovableRowStatus
|
||||
// ниже) — «Возвращена на доработку» уже означает, что своё слово по этому шагу
|
||||
// согласующий сказал (просто мы разрешаем потом переназначить человека), поэтому
|
||||
// двигать такой шаг/менять его параллельность нельзя, а переназначить — можно.
|
||||
const EDITABLE_ROW_STATUSES: string[] = [
|
||||
REQUEST_STATUS.draft,
|
||||
REQUEST_STATUS.queued,
|
||||
@@ -44,6 +49,22 @@ export function isEditableRowStatus(status: string): boolean {
|
||||
return EDITABLE_ROW_STATUSES.indexOf(status) !== -1;
|
||||
}
|
||||
|
||||
// Строка в одном из этих статусов можно двигать (стрелки вверх/вниз) и менять её
|
||||
// параллельность с соседней — строго подмножество EDITABLE_ROW_STATUSES, без
|
||||
// «Возвращена на доработку»: решение по шагу уже принято, менять его МЕСТО в
|
||||
// маршруте (а значит и то, когда он активируется повторно) не должно быть можно,
|
||||
// хотя переназначить исполнителя на нём — можно (см. isEditableRowStatus).
|
||||
const MOVABLE_ROW_STATUSES: string[] = [
|
||||
REQUEST_STATUS.draft,
|
||||
REQUEST_STATUS.queued,
|
||||
REQUEST_STATUS.onApproval,
|
||||
REQUEST_STATUS.onExecution
|
||||
];
|
||||
|
||||
export function isMovableRowStatus(status: string): boolean {
|
||||
return MOVABLE_ROW_STATUSES.indexOf(status) !== -1;
|
||||
}
|
||||
|
||||
// Заявка в этом статусе прошла полностью (все фазы завершены) — маршрут можно
|
||||
// только посмотреть, редактирование выключается целиком независимо от статусов
|
||||
// отдельных строк.
|
||||
|
||||
@@ -307,6 +307,7 @@ export const texts = {
|
||||
lockedHint: 'Решение уже принято — недоступно для правки.',
|
||||
ghostHint: 'Сохраняется…',
|
||||
phaseClosedHint: 'Эта фаза уже завершена — добавить в неё новый шаг нельзя.',
|
||||
requestRejectedHint: 'Заявка отклонена — добавить исполнителя нельзя.',
|
||||
moveUp: 'Шаг выше',
|
||||
moveDown: 'Шаг ниже',
|
||||
mergeWithPrev: 'Объединить с предыдущим шагом (выполнять параллельно)',
|
||||
|
||||
Reference in New Issue
Block a user