From 99f3558e4a3b2fe98d4b5a794b329f8a23cc88b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Tue, 13 Oct 2026 09:37:10 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BE=D0=B4=D0=B8=D0=BD=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B3=D0=BB=D0=B0=D1=81=D1=83=D1=8E=D1=89=D0=B8=D0=B9=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=D1=80=D1=83=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BD=D0=B0=20=D1=88=D0=B0=D0=B3=D0=B5=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B0=20=D0=B7=D0=B0=D1=8F?= =?UTF-8?q?=D0=B2=D0=BA=D0=B8=20=D0=B2=201=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit База 1С — мультивыбор, поэтому на одном шаге могут сработать сразу несколько строк матрицы с одним исполнителем (Гульжан и за ЗУП, и за АТП от финдепа). Он получал бы два уведомления и решал шаг дважды. Запланированный маршрут теперь оставляет одну строку на человека в пределах шага; один человек на разных шагах не схлопывается. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../forms/Ticket1C/Ticket1CForm.tsx | 13 ++++++--- .../forms/Ticket1C/dedupeRoute.test.ts | 28 +++++++++++++++++++ .../forms/Ticket1C/dedupeRoute.ts | 23 +++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.test.ts create mode 100644 src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.ts diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index af6b15f..8476f3f 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -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 = ({ 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 = ({ // в карточке 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] ); diff --git a/src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.test.ts b/src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.test.ts new file mode 100644 index 0000000..6a0eed4 --- /dev/null +++ b/src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.test.ts @@ -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); + }); +}); diff --git a/src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.ts b/src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.ts new file mode 100644 index 0000000..87dadac --- /dev/null +++ b/src/webparts/requestPortal/forms/Ticket1C/dedupeRoute.ts @@ -0,0 +1,23 @@ +import { IMatrixRow } from '../../shared/approval'; + +/** + * Убирает повторы одного и того же человека внутри одного шага маршрута. + * + * Зачем: «База 1С» — мультивыбор, и при нескольких базах на одном шаге могут + * сработать несколько строк матрицы с одним исполнителем (напр. Гульжан и за + * ЗУП, и за АТП от финдепа). Без схлопывания он получил бы две строки + * параллельного шага — два уведомления и два решения. + * + * Один человек на РАЗНЫХ шагах не схлопывается — это разные согласования. + * Строки без email (неназначенный шаг) не трогаем. + */ +export function dedupeRouteRows(rows: IMatrixRow[]): IMatrixRow[] { + const seen = new Set(); + 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; + }); +}