@@ -84,6 +84,44 @@ function renumberPhase(phaseRows: ManagedApprovalRow[]): Array<{ id: number; ord
. map ( r = > ( { id : r.id , order : r.order } ) ) ;
}
// Строки в одном из этих статусов ещё «в игре» — решение по ним не принято.
// approved/executed/rejected/returned сюда не входят: это уже принятые решения
// (в т.ч. «Возвращена на доработку» — согласующий своё слово сказал, просто мы
// разрешили его потом переназначить; в очереди на активацию он не участвует).
const PENDING_STATUSES : string [ ] = [ REQUEST_STATUS . queued , REQUEST_STATUS . onApproval , REQUEST_STATUS . onExecution ] ;
// Пересчитывает «чья сейчас очередь» в фазе — 1:1 activateNextQueued из
// request-portal (shared/approval/approvalList.service.ts): среди ещё не
// решённых строк у той, что с МИНИМАЛЬНЫМ order, выставляется активный статус
// фазы («На согласовании»/«На исполнении»), у всех остальных — «Ждёт очереди».
// Несколько строк с одинаковым order (параллельный шаг) активируются вместе —
// в этом и смысл параллельности. Вызывается после ЛЮБОГО изменения, которое
// могло поменять, кто в фазе «первый»: перестановка, удаление, добавление.
function computeActivationUpdates ( phaseRows : ManagedApprovalRow [ ] , phase : string ) : Array < { id : number ; status : string } > {
const activeStatus = phase === APPROVAL_PHASE . execution ? REQUEST_STATUS.onExecution : REQUEST_STATUS.onApproval ;
const pending = phaseRows . filter ( r = > PENDING_STATUSES . indexOf ( r . status ) !== - 1 ) ;
if ( pending . length === 0 ) return [ ] ;
const minOrder = Math . min ( . . . pending . map ( r = > r . order ) ) ;
const updates : Array < { id : number ; status : string } > = [ ] ;
pending . forEach ( r = > {
const desired = r . order === minOrder ? activeStatus : REQUEST_STATUS.queued ;
if ( r . status !== desired ) updates . push ( { id : r.id , status : desired } ) ;
} ) ;
return updates ;
}
// Сливает изменения order и status по одному и тому же id в один объект — иначе
// пришлось бы отправлять на одну и ту же строку два отдельных PATCH-запроса.
function mergeRowUpdates (
orderUpdates : Array < { id : number ; order : number } > ,
statusUpdates : Array < { id : number ; status : string } >
) : Array < { id : number ; order? : number ; status? : string } > {
const byId = new Map < number , { id : number ; order ? : number ; status ? : string } > ( ) ;
orderUpdates . forEach ( u = > byId . set ( u . id , { . . . ( byId . get ( u . id ) || { id : u.id } ) , order : u.order } ) ) ;
statusUpdates . forEach ( u = > byId . set ( u . id , { . . . ( byId . get ( u . id ) || { id : u.id } ) , status : u.status } ) ) ;
return Array . from ( byId . values ( ) ) ;
}
// Что выбрано в комбобоксах «Тип заявки»/«Заявка» — переживает перезагрузку
// страницы (localStorage), чтобы не искать одну и ту же заявку заново после
// каждого F5. selectedRequest хранится целиком (не только id), чтобы сразу
@@ -144,12 +182,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
// (и настоящий Id из SharePoint) ещё не долетел. Реальные Id списка всегда
// положительные, поэтому пересечься эти диапазоны не могут.
const nextGhostId = React . useRef ( - 1 ) ;
// Идёт фоновая запись (перестановка/переключение параллельности) — блокирует
// ТОЛЬКО повторную перестановку, чтобы не отправить второй reorder поверх ещё
// не завершённого (порядок мог бы разъехаться при гонке двух запросов).
// Переназначение/удаление/добавление такой блокировки не требуют: они не
// трогают order соседних строк, поэтому гонок между собой не создают.
const [ reordering , setReordering ] = React . useState ( false ) ;
const requestType = requestTypes . items . filter ( rt = > rt . id === Number ( requestTypeId ) ) [ 0 ] ;
// Только типы с привязанным списком SharePoint — без list_id/site_url искать
@@ -246,23 +278,36 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
const remainingPhaseRows = matrix . rows . filter ( r = > r . phase === row . phase && r . id !== row . id ) ;
const renumberUpdates = renumberPhase ( remainingPhaseRows ) ;
const orderById = new Map ( renumberUpdates . map ( u = > [ u . id , u . order ] ) ) ;
const renumberedPhaseRows = remainingPhaseRows . map ( r = > ( orderById . has ( r . id ) ? { . . . r , order : orderById.get ( r . id ) ! } : r ) ) ;
// Удалили как раз ту строку, что была активной («На согласовании» и т.п.) —
// очередь надо продвинуть на следующую по order, иначе фаза так и останется
// без единой активной строки.
const activationUpdates = computeActivationUpdates ( renumberedPhaseRows , row . phase ) ;
const statusById = new Map ( activationUpdates . map ( u = > [ u . id , u . status ] ) ) ;
const writeUpdates = mergeRowUpdates ( renumberUpdates , activationUpdates ) ;
setMatrix ( {
. . . matrix ,
rows : matrix.rows
. filter ( r = > r . id !== row . id )
. map ( r = > ( orderById . has ( r . id ) ? { . . . r , order : orderById.get ( r . id ) ! } : r ) )
. map ( r = > {
const order = orderById . get ( r . id ) ;
const status = statusById . get ( r . id ) ;
return order !== undefined || status !== undefined ? { . . . r , . . . ( order !== undefined && { order } ) , . . . ( status !== undefined && { status } ) } : r ;
} )
} ) ;
requestManage
. removeRow ( matrix , row . id )
. then ( ( ) = > {
if ( renumberUpdates . length === 0 ) return ;
// Удаление уже прошло — откатывать нечего, даже если это пересохранение
// номеров не удастся. Просто сообщаем об ошибке: строка гарантированно
// удалена, а нумерация в SharePoint в худшем случае останется с дыркой
// (что чинится обычной перестановкой любого шага той же фазы).
return requestManage . reorderRows ( matrix , renumberUpdates ) . catch ( ( e : Error ) = > setSaveError ( e . message ) ) ;
if ( writeUpdates . length === 0 ) return ;
// Удаление уже прошло — откатывать нечего, даже если пересохранение
// номеров/статусов не удастся. Просто сообщаем об ошибке: строка
// гарантированно удалена, а нумерация/активация в SharePoint в худшем
// случае разойдётся с тем, что видно на экране (починится обычной
// перестановкой любого шага той же фазы).
return requestManage . reorderRows ( matrix , writeUpdates ) . catch ( ( e : Error ) = > setSaveError ( e . message ) ) ;
} )
. catch ( ( e : Error ) = > {
setMatrix ( previous ) ;
@@ -321,13 +366,16 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
const order = nextOrder ( phaseRows ) ;
const ghostId = nextGhostId . current -- ;
// Новая строка всегда «Ждёт очереди» — request-portal переводит её в «На
// согласовании»/«На исполнении» через activateNextQueued при следующем
// решении согласующего или повторной отправке формы (см.
// request-portal/shared/approval/approvalList.service.ts). Если это ПЕРВЫЙ
// шаг фазы (например, добавили исполнителя в заявку, где фазы исполнения
// раньше не было вовсе) — activateNextQueued не срабо тае т сам по себе, пока
// кто-то не примет решение по заявке; шаг до этого момента просто ждёт.
// Новая строка встаёт ПОСЛЕДНЕЙ по order (см. nextOrder) — значит, стать
// активной сразу она может, только если во всей фазе вообще нет ни одной
// ожидающей строки (например, первый исполнитель, добавленный в заявку, где
// до этого фазы исполнения не было вовсе): тогда «первый по order среди
// ожидающих» — она и есть. Если в фазе уже кто-то ждёт своей очереди, та
// строка всегда раньше по order — новая просто вс таё т в конец «Ждёт очереди».
const hasPending = phaseRows . some ( r = > PENDING_STATUSES . indexOf ( r . status ) !== - 1 ) ;
const activeStatus = phase === APPROVAL_PHASE . execution ? REQUEST_STATUS.onExecution : REQUEST_STATUS.onApproval ;
const initialStatus = hasPending ? REQUEST_STATUS.queued : activeStatus ;
const ghostRow : ManagedApprovalRow = {
id : ghostId ,
ticketId : matrix.request.id ,
@@ -335,7 +383,7 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
approverName : found?.fio || email ,
approverEmail : email ,
position ,
status : REQUEST_STATUS.queued ,
status : initialStatus ,
date : '' ,
comment : '' ,
order ,
@@ -347,7 +395,7 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
setMatrix ( { . . . matrix , rows : [ . . . matrix . rows , ghostRow ] } ) ;
requestManage
. addRow ( matrix , matrix . request . id , phase , order , REQUEST_STATUS . queued , email , position , false )
. addRow ( matrix , matrix . request . id , phase , order , initialStatus , email , position , false )
. then ( ( ) = > reloadMatrix ( true ) )
. catch ( ( e : Error ) = > {
setMatrix ( previous ) ;
@@ -355,28 +403,38 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
} ) ;
} ;
// Перестановка/параллельность — оптимистично: order из ManageRequestsPhaseTable
// применяется к матрице СРАЗУ (клик по стрелке/значку виден мгновенно), запрос
// на запись уходит фоном. Если он упадёт — откатываем к состоянию до клика и
// показываем ошибку; если пройдёт — тихо перечитываем матрицу (не меняя
// видимого состояния, если её никто больше не менял, но подхватит, если менял).
// Перестановка/параллельность — оптимистично и БЕЗ блокировки: order из
// ManageRequestsPhaseTable применяется к матрице СРАЗУ (каждый к лик по
// стрелке/значку виден мгновенно, в т.ч. при быстрых повторных кликах —
// предыдущий фоновый запрос ничего не блокирует), запрос на запись уходит
// фоном. При ошибке — не откатываем к «состоянию до клика»: если кликов было
// несколько подряд, такой откат затёр бы более поздние, ещё не сохранённые
// изменения. Вместо этого просто перечитываем матрицу с сервера — она сама
// покажет, что в итоге реально сохранилось.
const handleReorder = ( updates : Array < { id : number ; order : number } > ) : void = > {
if ( ! matrix || reordering ) return ;
if ( ! matrix || updates . length === 0 ) return ;
setSaveError ( undefined ) ;
const previous = matrix ;
const orderById = new Map ( updates . map ( u = > [ u . id , u . order ] ) ) ;
setMatrix ( { . . . matrix , rows : matrix.rows.map ( r = > ( orderById . has ( r . id ) ? { . . . r , order : orderById.get ( r . id ) ! } : r ) ) } ) ;
const nextRows = matrix . rows . map ( r = > ( orderById . has ( r . id ) ? { . . . r , order : orderById.get ( r . id ) ! } : r ) ) ;
setReordering ( true ) ;
requestManage
. reorderRows ( matrix , updates )
. then ( ( ) = > reloadMatrix ( true ) )
. catch ( ( e : Error ) = > {
setMatrix ( previous ) ;
setSaveError ( e . message ) ;
} )
. finally ( ( ) = > setReordering ( false ) ) ;
// order-обновления всегда относятся к ОДНОЙ фазе (перестановка идёт внутри
// одной таблицы — см. ManageRequestsPhaseTable), поэтому фазу для пересчёта
// активации достаточно взять у любой из задетых строк.
const changedPhase = nextRows . filter ( r = > orderById . has ( r . id ) ) [ 0 ] . phase ;
const activationUpdates = computeActivationUpdates ( nextRows . filter ( r = > r . phase === changedPhase ) , changedPhase ) ;
const statusById = new Map ( activationUpdates . map ( u = > [ u . id , u . status ] ) ) ;
setMatrix ( {
. . . matrix ,
rows : nextRows.map ( r = > ( statusById . has ( r . id ) ? { . . . r , status : statusById.get ( r . id ) ! } : r ) )
} ) ;
const writeUpdates = mergeRowUpdates ( updates , activationUpdates ) ;
requestManage . reorderRows ( matrix , writeUpdates ) . catch ( ( e : Error ) = > {
setSaveError ( e . message ) ;
reloadMatrix ( true ) ;
} ) ;
} ;
return (
@@ -439,7 +497,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
title = { texts . manageRequests . approvalPhaseTitle }
rows = { approvalRows }
readOnly = { readOnly }
busy = { reordering }
statusVariant = { statusVariant }
onReassign = { openReassign }
onRemove = { askDelete }
@@ -453,7 +510,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
title = { texts . manageRequests . executionPhaseTitle }
rows = { executionRows }
readOnly = { readOnly }
busy = { reordering }
statusVariant = { statusVariant }
onReassign = { openReassign }
onRemove = { askDelete }