fix(ct): файл КП сохраняется с префиксом СТ — одноимённый КП другой СТ не перезаписывает его

КП загружались в корень библиотеки под исходным именем с перезаписью: «КП.pdf» одной СТ затирал
«КП.pdf» другой, и согласующий первой СТ открывал чужое КП. Теперь файл хранится как
«<id СТ>_<имя>» (buildKpStorageName); это имя идёт в поиск уже загруженного файла, поток, список
«Загруженные КП» и ссылку на файл. Повторная загрузка того же файла в эту же СТ по-прежнему
распознаётся.
This commit is contained in:
Тимур Городецкий
2026-10-01 17:03:14 +05:00
parent 56247fc98f
commit 04cd6fe690
5 changed files with 45 additions and 12 deletions
@@ -13,6 +13,7 @@ export interface KpFile {
file: File; file: File;
status: 'uploading' | 'extracting' | 'flowing' | 'done' | 'error'; status: 'uploading' | 'extracting' | 'flowing' | 'done' | 'error';
spId?: number; spId?: number;
storedName?: string; // имя в библиотеке КП (с префиксом СТ) — по нему ссылка на файл
supplierName?: string; supplierName?: string;
supplierSpId?: number; supplierSpId?: number;
supplierBIN?: string; supplierBIN?: string;
@@ -4,6 +4,7 @@ import { CTFormData } from '../hooks/useCtForm';
import { KpFile, FlowPositionResult, POLL_INTERVAL_MS, MAX_POLL_RETRIES } from './kpTypes'; import { KpFile, FlowPositionResult, POLL_INTERVAL_MS, MAX_POLL_RETRIES } from './kpTypes';
import { buildKpSuppliersAndOffers } from './step2Persist'; import { buildKpSuppliersAndOffers } from './step2Persist';
import { buildKpOffersToSave } from './kpOfferRecords'; import { buildKpOffersToSave } from './kpOfferRecords';
import { buildKpStorageName } from '../utils/kpFileName';
let localIdCounter = 0; let localIdCounter = 0;
@@ -51,7 +52,7 @@ export function useKpProcessing(
const currentUploaded = fd.uploadedKpFiles ?? []; const currentUploaded = fd.uploadedKpFiles ?? [];
const newEntries = doneFiles const newEntries = doneFiles
.filter(f => !currentUploaded.some(u => u.spId === f.spId)) .filter(f => !currentUploaded.some(u => u.spId === f.spId))
.map(f => ({ name: f.file.name, spId: f.spId as number, supplierName: f.supplierName })); .map(f => ({ name: f.storedName ?? f.file.name, spId: f.spId as number, supplierName: f.supplierName }));
if (newEntries.length > 0) { if (newEntries.length > 0) {
patch.uploadedKpFiles = [...currentUploaded, ...newEntries]; patch.uploadedKpFiles = [...currentUploaded, ...newEntries];
} }
@@ -193,13 +194,17 @@ export function useKpProcessing(
}; };
const handleFilesAdded = useCallback(async (files: File[]): Promise<void> => { const handleFilesAdded = useCallback(async (files: File[]): Promise<void> => {
const ctId = formDataRef.current.ct?.id;
// В «Загруженных КП» имена хранения (с префиксом СТ), в очереди — исходные: сверяем с обоими.
const existingNames = new Set([ const existingNames = new Set([
...kpFilesRef.current.map(f => f.file.name), ...kpFilesRef.current.map(f => f.file.name),
...(formDataRef.current.uploadedKpFiles ?? []).map(f => f.name), ...(formDataRef.current.uploadedKpFiles ?? []).map(f => f.name),
]); ]);
const isUploaded = (f: File): boolean =>
existingNames.has(f.name) || existingNames.has(buildKpStorageName(ctId, f.name));
const newFiles = files.filter(f => !existingNames.has(f.name)); const newFiles = files.filter(f => !isUploaded(f));
const skipped = files.filter(f => existingNames.has(f.name)); const skipped = files.filter(isUploaded);
if (skipped.length > 0) { if (skipped.length > 0) {
setDuplicateWarning(`Файл(ы) уже загружен(ы) в рамках этой СТ: ${skipped.map(f => f.name).join(', ')}`); setDuplicateWarning(`Файл(ы) уже загружен(ы) в рамках этой СТ: ${skipped.map(f => f.name).join(', ')}`);
@@ -222,25 +227,26 @@ export function useKpProcessing(
if (mountedRef.current) setKpFiles(prev => [...prev, ...entries]); if (mountedRef.current) setKpFiles(prev => [...prev, ...entries]);
for (const entry of entries) { for (const entry of entries) {
const storageName = buildKpStorageName(ctId, entry.file.name);
try { try {
let cachedSpId: number | undefined; let cachedSpId: number | undefined;
try { try {
const existing = await service.findExistingKpFile(entry.file.name, entry.file.size); const existing = await service.findExistingKpFile(storageName, entry.file.size);
if (existing) cachedSpId = existing.spId; if (existing) cachedSpId = existing.spId;
} catch { } catch {
// lookup failed — proceed with normal upload // lookup failed — proceed with normal upload
} }
if (cachedSpId !== undefined) { if (cachedSpId !== undefined) {
patchFile(entry.localId, { spId: cachedSpId, status: 'extracting' }); patchFile(entry.localId, { spId: cachedSpId, storedName: storageName, status: 'extracting' });
pollSyntex(entry.localId, cachedSpId, entry.file.name, 0).catch(err => { pollSyntex(entry.localId, cachedSpId, storageName, 0).catch(err => {
patchFile(entry.localId, { status: 'error', error: (err as Error).message }); patchFile(entry.localId, { status: 'error', error: (err as Error).message });
}); });
continue; continue;
} }
const { spId, name } = await service.uploadKpFile(entry.file); const { spId, name } = await service.uploadKpFile(entry.file, storageName);
patchFile(entry.localId, { spId, status: 'extracting' }); patchFile(entry.localId, { spId, storedName: name, status: 'extracting' });
const isWord = /\.(doc|docx|xls|xlsx)$/i.test(entry.file.name); const isWord = /\.(doc|docx|xls|xlsx)$/i.test(entry.file.name);
const isPdf = /\.pdf$/i.test(entry.file.name); const isPdf = /\.pdf$/i.test(entry.file.name);
@@ -0,0 +1,17 @@
import { buildKpStorageName } from './kpFileName';
describe('buildKpStorageName (имя файла КП в библиотеке)', () => {
// КП сохранялись под исходным именем с перезаписью: «КП.pdf» из одной СТ затирал «КП.pdf» другой,
// и согласующий первой СТ открывал чужое КП. Префикс СТ делает имя уникальным.
it('добавляет префикс СТ', () => {
expect(buildKpStorageName('357', 'КП.pdf')).toBe('357_КП.pdf');
});
it('не добавляет префикс второй раз', () => {
expect(buildKpStorageName('357', '357_КП.pdf')).toBe('357_КП.pdf');
});
it('без СТ — исходное имя', () => {
expect(buildKpStorageName(undefined, 'КП.pdf')).toBe('КП.pdf');
});
});
@@ -0,0 +1,8 @@
// Имя файла КП в библиотеке «Коммерческие предложения» — с префиксом СТ. Файлы лежат в корне библиотеки
// и загружались под исходным именем с перезаписью: «КП.pdf» одной СТ затирал «КП.pdf» другой, и
// согласующий открывал чужое КП. Ссылки, выгрузка INCORE и восстановление по ссылке работают по этому имени.
export function buildKpStorageName(ctId: string | undefined, originalName: string): string {
if (!ctId) return originalName;
const prefix = `${ctId}_`;
return originalName.startsWith(prefix) ? originalName : `${prefix}${originalName}`;
}
@@ -1002,9 +1002,10 @@ export class CtService {
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl);
} }
async uploadKpFile(file: File): Promise<{ spId: number; name: string }> { // storageName — имя в библиотеке (с префиксом СТ, см. buildKpStorageName); по умолчанию исходное.
async uploadKpFile(file: File, storageName: string = file.name): Promise<{ spId: number; name: string }> {
const buffer = await this._readFile(file); const buffer = await this._readFile(file);
const uploadUrl = `${this.baseUrl}/_api/web/GetFolderByServerRelativeUrl('${encodeURIComponent(this.kpFolder)}')/Files/add(url='${encodeURIComponent(file.name)}',overwrite=true)`; const uploadUrl = `${this.baseUrl}/_api/web/GetFolderByServerRelativeUrl('${encodeURIComponent(this.kpFolder)}')/Files/add(url='${encodeURIComponent(storageName)}',overwrite=true)`;
const uploadRes = await this.http.post( const uploadRes = await this.http.post(
uploadUrl, uploadUrl,
@@ -1020,7 +1021,7 @@ export class CtService {
if (!uploadRes.ok) if (!uploadRes.ok)
throw new Error(`Ошибка загрузки файла: ${uploadRes.status}`); throw new Error(`Ошибка загрузки файла: ${uploadRes.status}`);
const fileSRUrl = `${this.kpFolder}/${file.name}`; const fileSRUrl = `${this.kpFolder}/${storageName}`;
const itemRes = await this.http.get( const itemRes = await this.http.get(
`${this.baseUrl}/_api/web/GetFileByServerRelativeUrl('${encodeURIComponent(fileSRUrl)}')/ListItemAllFields?$select=Id`, `${this.baseUrl}/_api/web/GetFileByServerRelativeUrl('${encodeURIComponent(fileSRUrl)}')/ListItemAllFields?$select=Id`,
SPHttpClient.configurations.v1, SPHttpClient.configurations.v1,
@@ -1028,7 +1029,7 @@ export class CtService {
if (!itemRes.ok) if (!itemRes.ok)
throw new Error(`Ошибка получения ID файла: ${itemRes.status}`); throw new Error(`Ошибка получения ID файла: ${itemRes.status}`);
const itemData = await itemRes.json(); const itemData = await itemRes.json();
return { spId: Number(itemData.Id), name: file.name }; return { spId: Number(itemData.Id), name: storageName };
} }
async getKpItemFields(fileSpId: number): Promise<SyntexFields> { async getKpItemFields(fileSpId: number): Promise<SyntexFields> {