From 89b7b4ae5861173e3d6988efb3da90d36071b653 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Mon, 10 Aug 2026 09:50:03 +0500 Subject: [PATCH] =?UTF-8?q?=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=B7=D1=8F=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/ManageRequestsApproverModal.tsx | 14 +++++++++++++- .../screens/ManageRequestsPhaseTable.tsx | 9 ++++++++- .../components/screens/ManageRequestsScreen.tsx | 13 +++++++++++++ frontend/src/webparts/approvalAdmin/texts.ts | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx index 93e14dc..36947ce 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx @@ -10,6 +10,8 @@ export interface IManageRequestsApproverModalProps { initialEmail?: string; employees: Employee[]; employeesLoading: boolean; + /** Email'ы (в любом регистре), уже занятые другими шагами той же фазы — этот человек не может встать в фазу второй раз. */ + excludedEmails: string[]; onSave: (email: string, position: string) => Promise; onClose: () => void; } @@ -27,18 +29,28 @@ export const ManageRequestsApproverModal: React.FC { const [email, setEmail] = React.useState(initialEmail); const submit = useAsyncSubmit(); + // Тот, кто уже занят в этой фазе, исключён из списка выбора — дубликат + // физически нельзя выбрать (см. excludedEmails в ManageRequestsScreen). + const excludedSet = new Set(excludedEmails.map(e => e.toLowerCase())); + const selectableEmployees = employees.filter(e => !excludedSet.has(e.email.toLowerCase())); + const handleSubmit = (): void => { const trimmedEmail = email.trim(); if (!trimmedEmail) { submit.fail(texts.manageRequests.validationApprover); return; } + if (excludedSet.has(trimmedEmail.toLowerCase())) { + submit.fail(texts.manageRequests.validationDuplicateApprover); + return; + } const position = findEmployeeByEmail(employees, trimmedEmail)?.position || ''; submit.run(() => onSave(trimmedEmail, position)); }; @@ -48,7 +60,7 @@ export const ManageRequestsApproverModal: React.FC // добавлять туда исполнителя всё равно бессмысленно). const phaseClosed = requestRejected || (rows.length > 0 && rows.every(r => !isEditableRowStatus(r.status))); + // Один и тот же человек не может стоять в фазе дважды (в т.ч. призрачной + // строкой, которая ещё не подтверждена сервером, — см. handleAddApprover в + // ManageRequestsScreen) — исключаем уже занятых из пикера, а не проверяем + // постфактум: так дубликат физически нельзя выбрать. + const usedEmails = new Set(rows.map(r => r.approverEmail.toLowerCase()).filter(Boolean)); + const addableEmployees = employees.filter(e => !usedEmails.has(e.email.toLowerCase())); + const applyReorder = (mutate: (rows: Row[]) => Row[]): void => { const renumbered = withOrdersFrom(mutate(withParallel), 0); const updates = diffOrders(rows, renumbered); @@ -322,7 +329,7 @@ export const ManageRequestsPhaseTable: React.FC {!readOnly && !phaseClosed && (
= ({ api const openReassign = (row: ManagedApprovalRow): void => setModal(row); const closeModal = (): void => setModal(undefined); + // Кого нельзя выбрать при переназначении — все остальные согласующие/ + // исполнители ТОЙ ЖЕ фазы (сама строка modal исключена, иначе нельзя было + // бы оставить текущего человека на месте). Один человек не может стоять в + // фазе дважды — та же гарантия, что и addableEmployees в + // ManageRequestsPhaseTable для добавления нового шага. + const reassignExcludedEmails = matrix && modal + ? matrix.rows + .filter(r => r.phase === modal.phase && r.id !== modal.id) + .map(r => r.approverEmail.toLowerCase()) + .filter(Boolean) + : []; + // Переназначение — тоже оптимистично: patch конкретной строки сразу по ответу // сервера, без похода за всей матрицей заново. approverName берём из уже // загруженного справочника сотрудников (тот же источник, что подставляет @@ -572,6 +584,7 @@ export const ManageRequestsScreen: React.FC = ({ api initialEmail={modal.approverEmail} employees={employees.items} employeesLoading={employees.isLoading} + excludedEmails={reassignExcludedEmails} onSave={saveModal} onClose={closeModal} /> diff --git a/frontend/src/webparts/approvalAdmin/texts.ts b/frontend/src/webparts/approvalAdmin/texts.ts index ebee780..7c22ef6 100644 --- a/frontend/src/webparts/approvalAdmin/texts.ts +++ b/frontend/src/webparts/approvalAdmin/texts.ts @@ -321,6 +321,7 @@ export const texts = { approverLabel: 'Сотрудник', approverPlaceholder: 'Выберите сотрудника', validationApprover: 'Выберите сотрудника.', + validationDuplicateApprover: 'Этот сотрудник уже стоит в этой фазе на другом шаге.', deleteTitle: 'Удалить шаг?', deleteText: (position: string): string => `Шаг «${position || 'без должности'}» будет удалён безвозвратно.`, orderFloorHint: (order: number): string =>