feat(ct): ссылки на КП на шаге 3 для согласующих; участие в цепочке — по почте
Согласующий по ссылке из Teams попадает сразу на шаг 3, а ссылки на КП были только на шаге 2 — открыть КП при согласовании было негде. Теперь под таблицей сравнения блок «Коммерческие предложения» (поставщик → файл). Показывается тем же, кому видны цены: в КП тоже цены. Участие в цепочке согласования сверялось по ФИО — при другом написании согласующему скрывались цены (и теперь КП); сверяем по почте без учёта регистра, ФИО — запасной вариант.
This commit is contained in:
@@ -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<ICtCreateFormProps> = ({ 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(() => {
|
||||
|
||||
@@ -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<Props> = ({ files, service }) => {
|
||||
if (files.length === 0) return null;
|
||||
return (
|
||||
<div className={styles.criteriaSection}>
|
||||
<h4 className={styles.criteriaSectionTitle}>Коммерческие предложения</h4>
|
||||
<ul className={styles.noOfferSupplierList}>
|
||||
{files.map(file => (
|
||||
<li key={file.spId} className={styles.noOfferSupplierItem}>
|
||||
{file.supplierName && <span className={styles.noOfferSupplierName}>{file.supplierName}</span>}
|
||||
<a href={service.getKpFileUrl(file.name)} target="_blank" rel="noopener noreferrer">
|
||||
{file.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+4
@@ -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<Props> = ({
|
||||
|
||||
<NoOfferSuppliersList entries={getNoOfferSuppliers(formData.suppliers)} />
|
||||
|
||||
{/* В КП — цены: показываем тем же, кому видны цены (участникам согласования и автору). */}
|
||||
{!isRestrictedView && <KpFilesList files={formData.uploadedKpFiles ?? []} service={service} />}
|
||||
|
||||
<AiCommentBlock
|
||||
comment={ai.aiComment}
|
||||
decodedComment={ai.decodedAiComment}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { isUserInApprovalChain } from './approvalAccess';
|
||||
import { IMatrixRow } from '../../../../shared/approval';
|
||||
|
||||
function makeRow(overrides: Partial<IMatrixRow>): 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user