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; + }); +}