fix: один согласующий не дублируется на шаге маршрута заявки в 1С

База 1С — мультивыбор, поэтому на одном шаге могут сработать сразу
несколько строк матрицы с одним исполнителем (Гульжан и за ЗУП, и за
АТП от финдепа). Он получал бы два уведомления и решал шаг дважды.
Запланированный маршрут теперь оставляет одну строку на человека в
пределах шага; один человек на разных шагах не схлопывается.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-13 09:37:10 +05:00
parent a6f4218e7e
commit 99f3558e4a
3 changed files with 60 additions and 4 deletions
@@ -57,6 +57,7 @@ import { RequestSection } from './sections/RequestSection';
import { ContactSection } from './sections/ContactSection';
import { useStableCallback } from '../../shared/useStableCallback';
import { applyManagerOverride, managerEmail } from './managerOverride';
import { dedupeRouteRows } from './dedupeRoute';
import { handoffToPlanner } from './plannerHandoff';
import { submitStatus } from './submitStatus';
@@ -190,8 +191,10 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
isOpenedRouteReady(editId, requestStatus, requestTypeId) && (!!editId || (!!data.WorkType && !!requestTypeId));
const loadMatrix = useCallback(
() =>
loadRoutePhase(approval, editId, requestStatus, 'Approval', () =>
getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, fieldValues)
loadRoutePhase(approval, editId, requestStatus, 'Approval', async () =>
dedupeRouteRows(
await getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, fieldValues)
)
),
[context, userEmail, approval, editId, requestStatus, fieldValues, requestTypeId]
);
@@ -201,8 +204,10 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
// в карточке Planner, в портале отмечает «Выполнено» (см. plannerHandoff.ts).
const loadExecutors = useCallback(
() =>
loadRoutePhase(approval, editId, requestStatus, 'Execution', () =>
getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution', null, fieldValues)
loadRoutePhase(approval, editId, requestStatus, 'Execution', async () =>
dedupeRouteRows(
await getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution', null, fieldValues)
)
),
[context, userEmail, approval, editId, requestStatus, fieldValues, requestTypeId]
);
@@ -0,0 +1,28 @@
import { IMatrixRow } from '../../shared/approval';
import { dedupeRouteRows } from './dedupeRoute';
function row(order: number, email: string): IMatrixRow {
return { order, who: email, email, position: '', status: '—', date: '', comment: '', allowReassign: false };
}
describe('dedupeRouteRows', () => {
it('схлопывает одного человека на одном шаге (ЗУП + АТП от финдепа)', () => {
const rows = [row(1, 'boss@tnsu.kz'), row(2, 'gulzhan@tnsu.kz'), row(2, 'Gulzhan@tnsu.kz')];
expect(dedupeRouteRows(rows).map((r) => r.email)).toEqual(['boss@tnsu.kz', 'gulzhan@tnsu.kz']);
});
it('оставляет разных людей на одном шаге', () => {
const rows = [row(2, 'gulzhan@tnsu.kz'), row(2, 'zhanat@tnsu.kz')];
expect(dedupeRouteRows(rows)).toHaveLength(2);
});
it('оставляет одного человека на разных шагах', () => {
const rows = [row(1, 'gulzhan@tnsu.kz'), row(2, 'gulzhan@tnsu.kz')];
expect(dedupeRouteRows(rows)).toHaveLength(2);
});
it('не трогает неназначенные строки', () => {
const rows = [row(2, ''), row(2, '')];
expect(dedupeRouteRows(rows)).toHaveLength(2);
});
});
@@ -0,0 +1,23 @@
import { IMatrixRow } from '../../shared/approval';
/**
* Убирает повторы одного и того же человека внутри одного шага маршрута.
*
* Зачем: «База 1С» — мультивыбор, и при нескольких базах на одном шаге могут
* сработать несколько строк матрицы с одним исполнителем (напр. Гульжан и за
* ЗУП, и за АТП от финдепа). Без схлопывания он получил бы две строки
* параллельного шага — два уведомления и два решения.
*
* Один человек на РАЗНЫХ шагах не схлопывается — это разные согласования.
* Строки без email (неназначенный шаг) не трогаем.
*/
export function dedupeRouteRows(rows: IMatrixRow[]): IMatrixRow[] {
const seen = new Set<string>();
return rows.filter((row) => {
if (!row.email) return true;
const key = `${row.order}|${row.email.toLowerCase()}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}