fix: делегат на параллельном шаге получает заявку сразу
Очередь не двигалась, пока на шаге был кто-то активный, поэтому делегат участника параллельного шага ждал решения второго участника. Текущий шаг теперь — наименьший order среди нерешённых строк. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
|
|||||||
import { userSearchVariants } from './userSearchVariants';
|
import { userSearchVariants } from './userSearchVariants';
|
||||||
import { planRoutePatch } from './routePatchPlan';
|
import { planRoutePatch } from './routePatchPlan';
|
||||||
import { IStepOrderChange } from './queuedStepMove';
|
import { IStepOrderChange } from './queuedStepMove';
|
||||||
|
import { pickRowsToActivate } from './queueActivation';
|
||||||
|
|
||||||
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
||||||
const FIELDS = {
|
const FIELDS = {
|
||||||
@@ -240,9 +241,9 @@ export class ApprovalListService {
|
|||||||
await execute();
|
await execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Продвигает очередь: среди ещё не активных строк со статусом «Ждёт очереди»
|
// Продвигает очередь: в текущей фазе активирует ждущие строки текущего шага —
|
||||||
// в текущей фазе активирует те, что делят наименьший StepOrder (обычно одна
|
// наименьшего StepOrder среди нерешённых (обычно одна строка; несколько —
|
||||||
// строка; несколько — параллельный шаг). Фаза исполнения активируется, только
|
// параллельный шаг; правило см. pickRowsToActivate). Фаза исполнения активируется, только
|
||||||
// когда фаза согласования пройдена целиком (без Отклонено/Возвращена) — иначе
|
// когда фаза согласования пройдена целиком (без Отклонено/Возвращена) — иначе
|
||||||
// строки исполнения остаются «Ждёт очереди». Если фаза согласования уже
|
// строки исполнения остаются «Ждёт очереди». Если фаза согласования уже
|
||||||
// остановлена решением, ничего не активирует.
|
// остановлена решением, ничего не активирует.
|
||||||
@@ -260,13 +261,7 @@ export class ApprovalListService {
|
|||||||
approvalRows.length === 0 || approvalRows.every((r) => r.status === REQUEST_STATUS.approved);
|
approvalRows.length === 0 || approvalRows.every((r) => r.status === REQUEST_STATUS.approved);
|
||||||
|
|
||||||
const activate = async (phaseRows: IApprovalRow[], activeStatus: string): Promise<void> => {
|
const activate = async (phaseRows: IApprovalRow[], activeStatus: string): Promise<void> => {
|
||||||
if (phaseRows.some((r) => r.status === activeStatus)) return;
|
await Promise.all(pickRowsToActivate(phaseRows).map((id) => this.setActive(id, activeStatus)));
|
||||||
const queuedRows = phaseRows.filter((r) => r.status === REQUEST_STATUS.queued);
|
|
||||||
if (queuedRows.length === 0) return;
|
|
||||||
const minOrder = Math.min(...queuedRows.map((r) => r.order));
|
|
||||||
await Promise.all(
|
|
||||||
queuedRows.filter((r) => r.order === minOrder).map((r) => this.setActive(r.id, activeStatus))
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!approvalDone) {
|
if (!approvalDone) {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { IQueueRow, pickRowsToActivate } from './queueActivation';
|
||||||
|
import { REQUEST_STATUS } from './models/statuses';
|
||||||
|
|
||||||
|
function row(id: number, order: number, status: string = REQUEST_STATUS.queued): IQueueRow {
|
||||||
|
return { id, order, status };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('pickRowsToActivate', () => {
|
||||||
|
it('отправка: активируется первый шаг', () => {
|
||||||
|
expect(pickRowsToActivate([row(1, 1), row(2, 2)])).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('отправка: параллельный первый шаг активируется целиком', () => {
|
||||||
|
expect(pickRowsToActivate([row(1, 1), row(2, 1), row(3, 2)])).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('последовательный маршрут: пока шаг активен, очередь не двигается', () => {
|
||||||
|
expect(pickRowsToActivate([row(1, 1, REQUEST_STATUS.onApproval), row(2, 2)])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('шаг согласован — активируется следующий', () => {
|
||||||
|
expect(pickRowsToActivate([row(1, 1, REQUEST_STATUS.approved), row(2, 2)])).toEqual([2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('параллельный шаг: один согласовал, второй думает — следующий шаг ждёт', () => {
|
||||||
|
const rows = [row(1, 1, REQUEST_STATUS.approved), row(2, 1, REQUEST_STATUS.onApproval), row(3, 2)];
|
||||||
|
expect(pickRowsToActivate(rows)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('параллельный шаг: делегат активируется сразу, не дожидаясь второго участника', () => {
|
||||||
|
// Аня (1) переназначила на Дину (0.5) и ушла в очередь, Борис (2) ещё думает.
|
||||||
|
const rows = [row(4, 0.5), row(1, 1), row(2, 1, REQUEST_STATUS.onApproval), row(3, 2)];
|
||||||
|
expect(pickRowsToActivate(rows)).toEqual([4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('параллельный шаг: делегат согласовал — Аня возвращается в пару к Борису', () => {
|
||||||
|
const rows = [
|
||||||
|
row(4, 0.5, REQUEST_STATUS.approved),
|
||||||
|
row(1, 1),
|
||||||
|
row(2, 1, REQUEST_STATUS.onApproval),
|
||||||
|
row(3, 2),
|
||||||
|
];
|
||||||
|
expect(pickRowsToActivate(rows)).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('последовательный маршрут с делегатом: сначала делегат, потом делегировавший', () => {
|
||||||
|
expect(pickRowsToActivate([row(4, 0.5), row(1, 1), row(3, 2)])).toEqual([4]);
|
||||||
|
expect(pickRowsToActivate([row(4, 0.5, REQUEST_STATUS.approved), row(1, 1), row(3, 2)])).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('исполнение: тот же порядок', () => {
|
||||||
|
const rows = [row(1, 3, REQUEST_STATUS.executed), row(2, 4)];
|
||||||
|
expect(pickRowsToActivate(rows)).toEqual([2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('всё решено — активировать нечего', () => {
|
||||||
|
expect(pickRowsToActivate([row(1, 1, REQUEST_STATUS.approved)])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { IApprovalRow } from './models/IApprovalRow';
|
||||||
|
import { REQUEST_STATUS } from './models/statuses';
|
||||||
|
|
||||||
|
export type IQueueRow = Pick<IApprovalRow, 'id' | 'status' | 'order'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Какие строки ОДНОЙ фазы маршрута сделать активными. Чистая функция без
|
||||||
|
* SharePoint, выполняет ApprovalListService.activateNextQueued.
|
||||||
|
*
|
||||||
|
* Текущий шаг — наименьший StepOrder среди ещё не решённых строк (ждущих и
|
||||||
|
* активных). Активируются ждущие строки именно этого шага.
|
||||||
|
*
|
||||||
|
* Раньше правило было «есть активная строка — очередь не двигаем». На
|
||||||
|
* параллельном шаге это ломалось: если участник (Аня) переназначал заявку
|
||||||
|
* делегату (Дина встаёт перед Аней с дробным order), второй участник того же
|
||||||
|
* шага (Борис) всё ещё «На согласовании», и Дина не активировалась, пока не
|
||||||
|
* решит Борис. Теперь у Дины меньший order — она текущий шаг и активируется
|
||||||
|
* сразу, а после её решения Аня возвращается в пару к Борису.
|
||||||
|
*
|
||||||
|
* На последовательном маршруте поведение прежнее: пока шаг активен, ждущих
|
||||||
|
* строк с таким же или меньшим order нет — активировать нечего.
|
||||||
|
*/
|
||||||
|
export function pickRowsToActivate(phaseRows: IQueueRow[]): number[] {
|
||||||
|
const pending = phaseRows.filter(
|
||||||
|
(r) =>
|
||||||
|
r.status === REQUEST_STATUS.queued ||
|
||||||
|
r.status === REQUEST_STATUS.onApproval ||
|
||||||
|
r.status === REQUEST_STATUS.onExecution
|
||||||
|
);
|
||||||
|
if (pending.length === 0) return [];
|
||||||
|
|
||||||
|
const currentOrder = Math.min(...pending.map((r) => r.order));
|
||||||
|
return pending
|
||||||
|
.filter((r) => r.order === currentOrder && r.status === REQUEST_STATUS.queued)
|
||||||
|
.map((r) => r.id);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user