fix(ct): поставщик Шага 3 после черновика не задваивается при открытии СТ (#8)

«Сохранить черновик» оставляет поставщика, добавленного на Шаге 3, в списке
шага, а в SP он уже лежит колонкой «поставщик_заглушка». Если других
поставщиков в СТ нет, при открытии из реестра колонки собирались из SP — и
поставщик приходил дважды: из SP без цен и из сессии с ценами (подписи
колонок именем файла-заглушки .txt и «КП 2»). Теперь колонки из SP, которые
уже есть поставщиком Шага 3, не добавляются, а их офферы идут к нему.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-27 16:52:37 +05:00
parent 5c21718914
commit 09a0889741
3 changed files with 52 additions and 5 deletions
@@ -9,7 +9,7 @@ import { Step1PositionSelection } from './Step1PositionSelection/Step1PositionSe
import { Step2CommercialOffers } from './Step2CommercialOffers/Step2CommercialOffers'; import { Step2CommercialOffers } from './Step2CommercialOffers/Step2CommercialOffers';
import { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTable'; import { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTable';
import { Step4Approval } from './Step4Approval/Step4Approval'; import { Step4Approval } from './Step4Approval/Step4Approval';
import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord, findWinnerColumnId } from './utils/supplierColumns'; import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord, findWinnerColumnId, withoutSessionExtras } from './utils/supplierColumns';
import { buildOffersFromSpRecords } from './utils/spOffers'; import { buildOffersFromSpRecords } from './utils/spOffers';
import { excludedItemsToFormData } from './utils/excludedColumns'; import { excludedItemsToFormData } from './utils/excludedColumns';
import { isUserInApprovalChain } from './utils/approvalAccess'; import { isUserInApprovalChain } from './utils/approvalAccess';
@@ -122,16 +122,22 @@ const CtCreateForm: React.FC<ICtCreateFormProps> = ({ context, userDisplayName,
// КП в разных валютах. Используются, только когда в сессии колонок нет. // КП в разных валютах. Используются, только когда в сессии колонок нет.
const currencyCodeOf = (valutaSpId?: number): string => const currencyCodeOf = (valutaSpId?: number): string =>
valutaSpId ? (rateById.get(valutaSpId)?.code ?? 'KZT') : 'KZT'; valutaSpId ? (rateById.get(valutaSpId)?.code ?? 'KZT') : 'KZT';
const spSuppliers = buildColumnsFromSpOffers(rawOffers.map(o => ({ // Поставщики Шага 3 из сессии (после «Сохранить черновик») уже лежат в SP — их колонки из SP
// не дублируем.
const sessionExtras = formData.step3ExtraSuppliers ?? [];
const spSuppliers = withoutSessionExtras(buildColumnsFromSpOffers(rawOffers.map(o => ({
supplierSpId: o.supplierSpId, supplierSpId: o.supplierSpId,
supplierName: o.supplierName, supplierName: o.supplierName,
offerFileSpId: o.offerFileSpId, offerFileSpId: o.offerFileSpId,
currencyCode: currencyCodeOf(o.valutaSpId), currencyCode: currencyCodeOf(o.valutaSpId),
}))); }))), sessionExtras);
const supplierMap = new Map<number, string>(); const supplierMap = new Map<number, string>();
rawOffers.forEach(o => { if (!supplierMap.has(o.supplierSpId)) supplierMap.set(o.supplierSpId, o.supplierName); }); rawOffers.forEach(o => { if (!supplierMap.has(o.supplierSpId)) supplierMap.set(o.supplierSpId, o.supplierName); });
// Колонка оффера: из сессии, если она есть (в т.ч. старая колонка без файла КП), иначе — из SP. // Колонка оффера: из сессии, если она есть (в т.ч. старая колонка без файла КП), иначе — из SP.
const visibleColumns = formData.suppliers.length > 0 ? formData.suppliers : spSuppliers; // Поставщики Шага 3 — тоже колонки: их офферы из SP должны попасть к ним, а не в убранную колонку.
const visibleColumns = formData.suppliers.length > 0
? formData.suppliers
: [...spSuppliers, ...sessionExtras.map(es => ({ id: es.id, spId: es.spId, name: es.name, hasOffer: true }))];
const columnIdOf = (supplierSpId: number, offerFileSpId?: number): string => const columnIdOf = (supplierSpId: number, offerFileSpId?: number): string =>
findColumnForRecord(visibleColumns, supplierSpId, offerFileSpId)?.id findColumnForRecord(visibleColumns, supplierSpId, offerFileSpId)?.id
?? buildSupplierColumnId(supplierSpId, offerFileSpId); ?? buildSupplierColumnId(supplierSpId, offerFileSpId);
@@ -1,6 +1,31 @@
import { buildColumnDisplayNames, buildColumnLabels, buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord, findSiblingColumnIds, findWinnerColumnId } from './supplierColumns'; import { buildColumnDisplayNames, buildColumnLabels, buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord, findSiblingColumnIds, findWinnerColumnId, withoutSessionExtras } from './supplierColumns';
import { Supplier } from '../types/domain'; import { Supplier } from '../types/domain';
describe('withoutSessionExtras (поставщик Шага 3 после «Сохранить черновик»)', () => {
// СТ 375: единственный поставщик «fghfg» добавлен на Шаге 3, сохранён черновиком. При открытии из
// реестра колонка собиралась из SP («285_322», без цен) и ещё раз приходила из сессии (с ценами).
const fromSp: Supplier[] = [
{ id: '285_322', spId: 285, kpFileSpId: 322, name: 'fghfg', hasOffer: true },
{ id: '282_320', spId: 282, kpFileSpId: 320, name: 'Каспий Дорс', hasOffer: true },
];
it('колонку поставщика Шага 3 из SP не добавляет — она уже есть в сессии', () => {
const result = withoutSessionExtras(fromSp, [{ spId: 285, offerId: 322 }]);
expect(result.map(c => c.id)).toEqual(['282_320']);
});
it('другой КП того же поставщика — отдельная колонка, остаётся', () => {
const result = withoutSessionExtras(fromSp, [{ spId: 285, offerId: 999 }]);
expect(result.map(c => c.id)).toEqual(['285_322', '282_320']);
});
it('поставщик Шага 3 ещё не сохранён в SP — ничего не убирает', () => {
expect(withoutSessionExtras(fromSp, [{}])).toHaveLength(2);
});
});
describe('findWinnerColumnId (победитель из шапки СТ при открытии без сессии)', () => { describe('findWinnerColumnId (победитель из шапки СТ при открытии без сессии)', () => {
const columns: Supplier[] = [ const columns: Supplier[] = [
{ id: '281_318', spId: 281, kpFileSpId: 318, name: 'SteelGate', hasOffer: true, exclusion: { reason: 'Дорого' } }, { id: '281_318', spId: 281, kpFileSpId: 318, name: 'SteelGate', hasOffer: true, exclusion: { reason: 'Дорого' } },
@@ -44,6 +44,22 @@ export function buildColumnsFromSpOffers(records: SpOfferColumnSource[]): Suppli
return Array.from(columnsById.values()); return Array.from(columnsById.values());
} }
// Поставщик, добавленный на Шаге 3, в сессии: SP Id поставщика и его файла-заглушки КП.
export interface SessionExtraColumn {
spId?: number;
offerId?: number;
}
// Колонки из SP без тех, что в сессии уже есть поставщиком Шага 3. «Сохранить черновик» оставляет
// такого поставщика в списке Шага 3 (см. buildSuppliersPatch), а в SP он уже лежит колонкой
// «поставщик_заглушка». Без этого при открытии СТ, где других поставщиков нет, колонка задваивалась:
// одна из SP без цен, вторая из сессии с ценами.
export function withoutSessionExtras(spColumns: Supplier[], extras: SessionExtraColumn[]): Supplier[] {
return spColumns.filter(column => !extras.some(extra =>
extra.spId !== undefined && extra.spId === column.spId && extra.offerId === column.kpFileSpId,
));
}
// Колонка победителя по winSupplier из шапки СТ (СТ открыта без сессии). В SP хранится поставщик, // Колонка победителя по winSupplier из шапки СТ (СТ открыта без сессии). В SP хранится поставщик,
// а не колонка: у поставщика с несколькими КП берём первую неисключённую колонку. // а не колонка: у поставщика с несколькими КП берём первую неисключённую колонку.
export function findWinnerColumnId(columns: Supplier[], winSupplierSpId: number | undefined): string | undefined { export function findWinnerColumnId(columns: Supplier[], winSupplierSpId: number | undefined): string | undefined {