fix(ct): файлы-заглушки КП не видны в списках и не идут в пакет INCORE (#10)
Поставщику, добавленному на Шаге 3 вручную, система создаёт в библиотеке КП файл-заглушку .txt — оффер в SP должен ссылаться на файл КП. Он показывался в «Коммерческих предложениях», в «Загруженных КП» Шага 2, в подписи колонки и скачивался в пакет INCORE. Теперь при показе и выгрузке заглушки скрыты (узнаём по .txt — на Шаге 2 загружают только pdf/docx/doc), в подписи колонки — «Добавлен вручную». Данные формы не меняются: по ним сохранение находит файл. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-2
@@ -14,6 +14,7 @@ import { UploadedKpList } from './UploadedKpList';
|
|||||||
import { removeKpFileFromComparison } from './kpRemoval';
|
import { removeKpFileFromComparison } from './kpRemoval';
|
||||||
import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency';
|
import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency';
|
||||||
import { SaveDraftButton } from '../SaveDraftButton/SaveDraftButton';
|
import { SaveDraftButton } from '../SaveDraftButton/SaveDraftButton';
|
||||||
|
import { withoutPlaceholders } from '../utils/placeholderKpFile';
|
||||||
import styles from './Step2CommercialOffers.module.scss';
|
import styles from './Step2CommercialOffers.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -160,6 +161,8 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canProceed = !isSaving && currencyRates.every(r => r.rateToKzt > 0);
|
const canProceed = !isSaving && currencyRates.every(r => r.rateToKzt > 0);
|
||||||
|
// Заглушки поставщиков, добавленных на Шаге 3 вручную, — не КП: в списке не показываем.
|
||||||
|
const realKpFiles = withoutPlaceholders(formData.uploadedKpFiles ?? []);
|
||||||
const currencyOptions = ['KZT', ...currencyRates.filter(r => r.code !== 'KZT').map(r => r.code)];
|
const currencyOptions = ['KZT', ...currencyRates.filter(r => r.code !== 'KZT').map(r => r.code)];
|
||||||
|
|
||||||
const uploadedFiles: UploadedFile[] = kp.kpFiles.map(f => ({
|
const uploadedFiles: UploadedFile[] = kp.kpFiles.map(f => ({
|
||||||
@@ -201,9 +204,9 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
|||||||
|
|
||||||
<SuppliersWithoutKp items={noOfferSuppliers} onChange={handleNoOfferChange} disabled={isReadOnly} />
|
<SuppliersWithoutKp items={noOfferSuppliers} onChange={handleNoOfferChange} disabled={isReadOnly} />
|
||||||
|
|
||||||
{!isRestrictedView && (formData.uploadedKpFiles?.length ?? 0) > 0 && (
|
{!isRestrictedView && realKpFiles.length > 0 && (
|
||||||
<UploadedKpList
|
<UploadedKpList
|
||||||
files={formData.uploadedKpFiles}
|
files={realKpFiles}
|
||||||
service={service}
|
service={service}
|
||||||
currencyOptions={currencyOptions}
|
currencyOptions={currencyOptions}
|
||||||
getCurrency={file => getSupplierCurrency(formData.suppliers ?? [], file)}
|
getCurrency={file => getSupplierCurrency(formData.suppliers ?? [], file)}
|
||||||
|
|||||||
+3
-1
@@ -20,6 +20,7 @@ import { useRecommendedPick } from './useRecommendedPick';
|
|||||||
import { VendorPickerDialog } from './VendorPickerDialog';
|
import { VendorPickerDialog } from './VendorPickerDialog';
|
||||||
import { ExcludedSuppliersList } from './ExcludedSuppliersList';
|
import { ExcludedSuppliersList } from './ExcludedSuppliersList';
|
||||||
import { SaveDraftButton } from '../SaveDraftButton/SaveDraftButton';
|
import { SaveDraftButton } from '../SaveDraftButton/SaveDraftButton';
|
||||||
|
import { withoutPlaceholders } from '../utils/placeholderKpFile';
|
||||||
|
|
||||||
// Исключённых поставщиков в режиме просмотра (согласующий, отправленная СТ) пока не показываем —
|
// Исключённых поставщиков в режиме просмотра (согласующий, отправленная СТ) пока не показываем —
|
||||||
// решение от 2026-10-27 (docs/adr/0001). Данные грузятся и так: включить — поменять на true.
|
// решение от 2026-10-27 (docs/adr/0001). Данные грузятся и так: включить — поменять на true.
|
||||||
@@ -202,7 +203,8 @@ export const Step3ComparisonTable: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* В КП — цены: показываем тем же, кому видны цены (участникам согласования и автору). */}
|
{/* В КП — цены: показываем тем же, кому видны цены (участникам согласования и автору). */}
|
||||||
{!isRestrictedView && <KpFilesList files={formData.uploadedKpFiles ?? []} service={service} />}
|
{/* Заглушки поставщиков, добавленных вручную, — не КП: не показываем. */}
|
||||||
|
{!isRestrictedView && <KpFilesList files={withoutPlaceholders(formData.uploadedKpFiles ?? [])} service={service} />}
|
||||||
|
|
||||||
<AiCommentBlock
|
<AiCommentBlock
|
||||||
comment={ai.aiComment}
|
comment={ai.aiComment}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { CtService } from '../../services/CtService';
|
import { CtService } from '../../services/CtService';
|
||||||
import { SigexSignService, SigexSignStep } from '../../services/SigexSignService';
|
import { SigexSignService, SigexSignStep } from '../../services/SigexSignService';
|
||||||
import { CTFormData } from '../hooks/useCtForm';
|
import { CTFormData } from '../hooks/useCtForm';
|
||||||
|
import { withoutPlaceholders } from '../utils/placeholderKpFile';
|
||||||
|
|
||||||
// Этапы выгрузки пакета в INCORE: формирование отчёта + этапы подписания SIGEX.
|
// Этапы выгрузки пакета в INCORE: формирование отчёта + этапы подписания SIGEX.
|
||||||
export type IncoreStep = 'report' | SigexSignStep;
|
export type IncoreStep = 'report' | SigexSignStep;
|
||||||
@@ -72,9 +73,10 @@ export function useIncore(args: UseIncoreArgs) {
|
|||||||
setCardAttachment(saved);
|
setCardAttachment(saved);
|
||||||
|
|
||||||
// 4. Пакет для INCORE: карточка с ЭЦП + файлы КП — скачиваем пользователю.
|
// 4. Пакет для INCORE: карточка с ЭЦП + файлы КП — скачиваем пользователю.
|
||||||
|
// Заглушки поставщиков, добавленных вручную, — не КП, в пакет не идут.
|
||||||
service.downloadBlob(cardBlob, incoreCardName);
|
service.downloadBlob(cardBlob, incoreCardName);
|
||||||
|
|
||||||
const kpFiles = formData.uploadedKpFiles ?? [];
|
const kpFiles = withoutPlaceholders(formData.uploadedKpFiles ?? []);
|
||||||
for (const f of kpFiles) {
|
for (const f of kpFiles) {
|
||||||
await service.downloadKpFile(f.name);
|
await service.downloadKpFile(f.name);
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, 300));
|
await new Promise<void>((resolve) => setTimeout(resolve, 300));
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { isPlaceholderKpFile, withoutPlaceholders } from './placeholderKpFile';
|
||||||
|
|
||||||
|
describe('isPlaceholderKpFile', () => {
|
||||||
|
// СТ 375: поставщик «fghfg» добавлен на Шаге 3 — заглушка видна в «Коммерческих предложениях».
|
||||||
|
it('заглушка поставщика, добавленного вручную', () => {
|
||||||
|
expect(isPlaceholderKpFile('fghfg_1793100061349.txt')).toBe(true);
|
||||||
|
expect(isPlaceholderKpFile('ООО Альфа_1793100061349.TXT')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('настоящие КП — не заглушки', () => {
|
||||||
|
expect(isPlaceholderKpFile('372_KP_SteelGate_Export_USD.pdf')).toBe(false);
|
||||||
|
expect(isPlaceholderKpFile('372_KP_TOO_Kaspiy_Dors.docx')).toBe(false);
|
||||||
|
expect(isPlaceholderKpFile('КП.doc')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('withoutPlaceholders', () => {
|
||||||
|
it('оставляет только настоящие КП, порядок сохраняется', () => {
|
||||||
|
const files = [
|
||||||
|
{ spId: 318, name: '372_KP_SteelGate.pdf' },
|
||||||
|
{ spId: 322, name: 'fghfg_1793100061349.txt' },
|
||||||
|
{ spId: 320, name: '372_KP_Kaspiy.docx' },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(withoutPlaceholders(files).map(f => f.spId)).toEqual([318, 320]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Файл-заглушка КП: создаётся для поставщика, добавленного на Шаге 3 вручную, — оффер в SP должен
|
||||||
|
// ссылаться на файл КП (CtService._createPlaceholderKpFile). Людям он не нужен: в списках КП и в
|
||||||
|
// пакете INCORE его не показываем. Узнаём по расширению: на Шаге 2 загрузить можно только
|
||||||
|
// .pdf/.docx/.doc (FileUploadArea), а заглушка всегда .txt.
|
||||||
|
const PLACEHOLDER_EXTENSION = '.txt';
|
||||||
|
|
||||||
|
// Подпись колонки поставщика, у которого вместо КП — заглушка.
|
||||||
|
export const PLACEHOLDER_COLUMN_LABEL = 'Добавлен вручную';
|
||||||
|
|
||||||
|
export function isPlaceholderKpFile(fileName: string): boolean {
|
||||||
|
return fileName.toLowerCase().endsWith(PLACEHOLDER_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настоящие КП — для показа и выгрузки. Данные формы (uploadedKpFiles) не фильтруем: по ним
|
||||||
|
// сохранение находит файл КП колонки, в том числе заглушку.
|
||||||
|
export function withoutPlaceholders<T extends { name: string }>(files: T[]): T[] {
|
||||||
|
return files.filter(file => !isPlaceholderKpFile(file.name));
|
||||||
|
}
|
||||||
@@ -93,6 +93,19 @@ describe('buildColumnsFromSpOffers (открытие по ссылке / без
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('подпись колонки с файлом-заглушкой', () => {
|
||||||
|
// СТ 375: в шапке колонки показывалось имя заглушки «fghfg_1793100061349.txt».
|
||||||
|
it('вместо имени заглушки — «Добавлен вручную»', () => {
|
||||||
|
const columns: Supplier[] = [
|
||||||
|
{ id: '285_322', spId: 285, kpFileSpId: 322, name: 'fghfg', hasOffer: true },
|
||||||
|
{ id: '285_330', spId: 285, kpFileSpId: 330, name: 'fghfg', hasOffer: true },
|
||||||
|
];
|
||||||
|
const files = [{ spId: 322, name: 'fghfg_1793100061349.txt' }, { spId: 330, name: '375_KP_fghfg.pdf' }];
|
||||||
|
|
||||||
|
expect(buildColumnLabels(columns, files)).toEqual({ '285_322': 'Добавлен вручную', '285_330': '375_KP_fghfg.pdf' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('подписи колонок поставщика с несколькими КП', () => {
|
describe('подписи колонок поставщика с несколькими КП', () => {
|
||||||
const ARLAN = 'ТОО «Арлан Офис Снаб»';
|
const ARLAN = 'ТОО «Арлан Офис Снаб»';
|
||||||
const columns: Supplier[] = [
|
const columns: Supplier[] = [
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Supplier } from '../types/domain';
|
import { Supplier } from '../types/domain';
|
||||||
|
import { PLACEHOLDER_COLUMN_LABEL, isPlaceholderKpFile } from './placeholderKpFile';
|
||||||
|
|
||||||
// Колонка таблицы сравнения = поставщик + файл КП: у одного поставщика может быть несколько
|
// Колонка таблицы сравнения = поставщик + файл КП: у одного поставщика может быть несколько
|
||||||
// предложений (напр. в тенге и в долларах). Разделитель «_», а не «::» — «::» уже служит
|
// предложений (напр. в тенге и в долларах). Разделитель «_», а не «::» — «::» уже служит
|
||||||
@@ -91,6 +92,11 @@ export function buildColumnLabels(
|
|||||||
if (group.length < 2) return;
|
if (group.length < 2) return;
|
||||||
group.forEach((c, index) => {
|
group.forEach((c, index) => {
|
||||||
const fileName = uploadedKpFiles.find(f => f.spId === c.kpFileSpId)?.name;
|
const fileName = uploadedKpFiles.find(f => f.spId === c.kpFileSpId)?.name;
|
||||||
|
// Имя файла-заглушки (поставщик добавлен вручную) людям ничего не говорит.
|
||||||
|
if (fileName !== undefined && isPlaceholderKpFile(fileName)) {
|
||||||
|
labels[c.id] = PLACEHOLDER_COLUMN_LABEL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
labels[c.id] = fileName ?? `КП ${index + 1}`;
|
labels[c.id] = fileName ?? `КП ${index + 1}`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user