From 56247fc98fcdbcef175e523db1404fef24e28385 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: Thu, 1 Oct 2026 16:56:40 +0500 Subject: [PATCH] =?UTF-8?q?feat(ct):=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D0=9A=D0=9F=20=D0=BD=D0=B0=20=D1=88?= =?UTF-8?q?=D0=B0=D0=B3=D0=B5=203=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B3=D0=BB=D0=B0=D1=81=D1=83=D1=8E=D1=89=D0=B8=D1=85;=20?= =?UTF-8?q?=D1=83=D1=87=D0=B0=D1=81=D1=82=D0=B8=D0=B5=20=D0=B2=20=D1=86?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE=D1=87=D0=BA=D0=B5=20=E2=80=94=20=D0=BF=D0=BE?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=87=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Согласующий по ссылке из Teams попадает сразу на шаг 3, а ссылки на КП были только на шаге 2 — открыть КП при согласовании было негде. Теперь под таблицей сравнения блок «Коммерческие предложения» (поставщик → файл). Показывается тем же, кому видны цены: в КП тоже цены. Участие в цепочке согласования сверялось по ФИО — при другом написании согласующему скрывались цены (и теперь КП); сверяем по почте без учёта регистра, ФИО — запасной вариант. --- .../forms/ct/components/CtCreateForm.tsx | 11 +++--- .../Step3ComparisonTable/KpFilesList.tsx | 30 ++++++++++++++++ .../Step3ComparisonTable.tsx | 4 +++ .../components/utils/approvalAccess.test.ts | 35 +++++++++++++++++++ .../ct/components/utils/approvalAccess.ts | 18 ++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/KpFilesList.tsx create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/utils/approvalAccess.test.ts create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/utils/approvalAccess.ts diff --git a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx index d4feb44..6591caf 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx @@ -10,6 +10,7 @@ import { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTabl import { Step4Approval } from './Step4Approval/Step4Approval'; import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns'; import { buildOffersFromSpRecords } from './utils/spOffers'; +import { isUserInApprovalChain } from './utils/approvalAccess'; import { noOfferRecordsToSuppliers, withNoOfferSuppliers } from './utils/noOfferSuppliers'; import styles from './CtCreateForm.module.scss'; import '@tnsu/ui-kit/styles.css'; @@ -33,12 +34,14 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, ensureHighestStep, } = useCtForm(); - // Цены показываем только участникам согласования: рекомендующему (Инициатор) и согласующим. + // Цены и КП показываем только участникам согласования: рекомендующему (Инициатор) и согласующим. // Автор/инициатор при создании и редактировании СТ (до отправки на согласование) видит цены всегда. // astel-k (isRestrictedView) — независимое ограничение, складывается с этим: если его уберут, - // правило по цепочке согласования останется. Сопоставление участника — по отображаемому имени - // (пока тестируем; при необходимости переключим на ID). - const userInApprovalChain = (formData.approvalMatrix ?? []).some(m => m.who === userDisplayName); + // правило по цепочке согласования останется. Участника ищем по почте (ФИО — запасной вариант). + const userInApprovalChain = isUserInApprovalChain( + formData.approvalMatrix ?? [], + { email: service.currentUserEmail, displayName: userDisplayName }, + ); const hidePrices = isRestrictedView || (formData.approvalSent === true && !userInApprovalChain); useEffect(() => { diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/KpFilesList.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/KpFilesList.tsx new file mode 100644 index 0000000..540da73 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/KpFilesList.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { CtService } from '../../services/CtService'; +import { CTFormData } from '../hooks/useCtForm'; +import styles from './Step3ComparisonTable.module.scss'; + +interface Props { + files: CTFormData['uploadedKpFiles']; + service: CtService; +} + +// Ссылки на файлы КП под таблицей сравнения. Согласующий по ссылке из Teams попадает сразу на шаг 3, +// а список КП раньше был только на шаге 2 — открыть КП при согласовании было негде. +export const KpFilesList: React.FC = ({ files, service }) => { + if (files.length === 0) return null; + return ( +
+

Коммерческие предложения

+
    + {files.map(file => ( +
  • + {file.supplierName && {file.supplierName}} + + {file.name} + +
  • + ))} +
+
+ ); +}; diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx index 3503f3e..028dac1 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx @@ -13,6 +13,7 @@ import { useStep3Matrix } from './useStep3Matrix'; import { ComparisonMatrix } from './ComparisonMatrix'; import { CriteriaTable } from './CriteriaTable'; import { NoOfferSuppliersList } from './NoOfferSuppliersList'; +import { KpFilesList } from './KpFilesList'; import { getNoOfferSuppliers } from '../utils/noOfferSuppliers'; import { useAdataCheck } from './useAdataCheck'; import { useRecommendedPick } from './useRecommendedPick'; @@ -177,6 +178,9 @@ export const Step3ComparisonTable: React.FC = ({ + {/* В КП — цены: показываем тем же, кому видны цены (участникам согласования и автору). */} + {!isRestrictedView && } + ): IMatrixRow { + return { + order: 1, who: 'Иванов Иван', email: 'ivanov@tnsu.kz', position: 'Директор', status: 'На согласовании', + date: '', comment: '', allowReassign: false, ...overrides, + }; +} + +describe('isUserInApprovalChain (кому видны цены и КП при согласовании)', () => { + // Раньше сверяли только по ФИО: «Иванов Иван» в SP и «Иван Иванов» в профиле — согласующему + // скрывались цены и КП, хотя он в цепочке. + it('находит согласующего по почте, даже если ФИО записано иначе', () => { + const matrix = [makeRow({ who: 'Иванов Иван' })]; + + expect(isUserInApprovalChain(matrix, { email: 'IVANOV@tnsu.kz', displayName: 'Иван Иванов' })).toBe(true); + }); + + it('без почты в строке — сверяет по ФИО', () => { + const matrix = [makeRow({ email: '' })]; + + expect(isUserInApprovalChain(matrix, { email: 'ivanov@tnsu.kz', displayName: 'Иванов Иван' })).toBe(true); + }); + + it('не в цепочке — false', () => { + const matrix = [makeRow({})]; + + expect(isUserInApprovalChain(matrix, { email: 'petrov@tnsu.kz', displayName: 'Петров Пётр' })).toBe(false); + }); + + it('пустая цепочка — false', () => { + expect(isUserInApprovalChain([], { email: 'ivanov@tnsu.kz', displayName: 'Иванов Иван' })).toBe(false); + }); +}); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/approvalAccess.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/approvalAccess.ts new file mode 100644 index 0000000..ef396a0 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/approvalAccess.ts @@ -0,0 +1,18 @@ +import { IMatrixRow } from '../../../../shared/approval'; + +export interface CurrentUser { + email: string; + displayName: string; +} + +// Участник цепочки согласования — ему видны цены и КП. Сверяем по почте (без учёта регистра): ФИО в SP +// и в профиле пишут по-разному («Иванов Иван» / «Иван Иванов»), и по одному ФИО согласующему цены и КП +// скрывались. ФИО — запасной вариант для строк без почты. +export function isUserInApprovalChain(matrix: IMatrixRow[], user: CurrentUser): boolean { + const email = user.email.trim().toLowerCase(); + return matrix.some(row => { + const rowEmail = (row.email ?? '').trim().toLowerCase(); + if (rowEmail && email) return rowEmail === email; + return row.who === user.displayName; + }); +}