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