From 04cd6fe69070dcf7c68593ba41df0521a39823db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Thu, 1 Oct 2026 17:03:14 +0500 Subject: [PATCH] =?UTF-8?q?fix(ct):=20=D1=84=D0=B0=D0=B9=D0=BB=20=D0=9A?= =?UTF-8?q?=D0=9F=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=8F=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D1=81=20=D0=BF=D1=80=D0=B5=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=D0=BE=D0=BC=20=D0=A1=D0=A2=20=E2=80=94=20=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D0=BE=D0=B8=D0=BC=D1=91=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=9A?= =?UTF-8?q?=D0=9F=20=D0=B4=D1=80=D1=83=D0=B3=D0=BE=D0=B9=20=D0=A1=D0=A2=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit КП загружались в корень библиотеки под исходным именем с перезаписью: «КП.pdf» одной СТ затирал «КП.pdf» другой, и согласующий первой СТ открывал чужое КП. Теперь файл хранится как «_<имя>» (buildKpStorageName); это имя идёт в поиск уже загруженного файла, поток, список «Загруженные КП» и ссылку на файл. Повторная загрузка того же файла в эту же СТ по-прежнему распознаётся. --- .../Step2CommercialOffers/kpTypes.ts | 1 + .../Step2CommercialOffers/useKpProcessing.ts | 22 ++++++++++++------- .../ct/components/utils/kpFileName.test.ts | 17 ++++++++++++++ .../forms/ct/components/utils/kpFileName.ts | 8 +++++++ .../forms/ct/services/CtService.ts | 9 ++++---- 5 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.test.ts create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.ts diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts index bc49813..225dab6 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts @@ -13,6 +13,7 @@ export interface KpFile { file: File; status: 'uploading' | 'extracting' | 'flowing' | 'done' | 'error'; spId?: number; + storedName?: string; // имя в библиотеке КП (с префиксом СТ) — по нему ссылка на файл supplierName?: string; supplierSpId?: number; supplierBIN?: string; diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts index 20125c4..4244e7e 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts @@ -4,6 +4,7 @@ import { CTFormData } from '../hooks/useCtForm'; import { KpFile, FlowPositionResult, POLL_INTERVAL_MS, MAX_POLL_RETRIES } from './kpTypes'; import { buildKpSuppliersAndOffers } from './step2Persist'; import { buildKpOffersToSave } from './kpOfferRecords'; +import { buildKpStorageName } from '../utils/kpFileName'; let localIdCounter = 0; @@ -51,7 +52,7 @@ export function useKpProcessing( const currentUploaded = fd.uploadedKpFiles ?? []; const newEntries = doneFiles .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) { patch.uploadedKpFiles = [...currentUploaded, ...newEntries]; } @@ -193,13 +194,17 @@ export function useKpProcessing( }; const handleFilesAdded = useCallback(async (files: File[]): Promise => { + const ctId = formDataRef.current.ct?.id; + // В «Загруженных КП» имена хранения (с префиксом СТ), в очереди — исходные: сверяем с обоими. const existingNames = new Set([ ...kpFilesRef.current.map(f => f.file.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 skipped = files.filter(f => existingNames.has(f.name)); + const newFiles = files.filter(f => !isUploaded(f)); + const skipped = files.filter(isUploaded); if (skipped.length > 0) { setDuplicateWarning(`Файл(ы) уже загружен(ы) в рамках этой СТ: ${skipped.map(f => f.name).join(', ')}`); @@ -222,25 +227,26 @@ export function useKpProcessing( if (mountedRef.current) setKpFiles(prev => [...prev, ...entries]); for (const entry of entries) { + const storageName = buildKpStorageName(ctId, entry.file.name); try { let cachedSpId: number | undefined; 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; } catch { // lookup failed — proceed with normal upload } if (cachedSpId !== undefined) { - patchFile(entry.localId, { spId: cachedSpId, status: 'extracting' }); - pollSyntex(entry.localId, cachedSpId, entry.file.name, 0).catch(err => { + patchFile(entry.localId, { spId: cachedSpId, storedName: storageName, status: 'extracting' }); + pollSyntex(entry.localId, cachedSpId, storageName, 0).catch(err => { patchFile(entry.localId, { status: 'error', error: (err as Error).message }); }); continue; } - const { spId, name } = await service.uploadKpFile(entry.file); - patchFile(entry.localId, { spId, status: 'extracting' }); + const { spId, name } = await service.uploadKpFile(entry.file, storageName); + patchFile(entry.localId, { spId, storedName: name, status: 'extracting' }); const isWord = /\.(doc|docx|xls|xlsx)$/i.test(entry.file.name); const isPdf = /\.pdf$/i.test(entry.file.name); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.test.ts new file mode 100644 index 0000000..b8158dd --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.test.ts @@ -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'); + }); +}); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.ts new file mode 100644 index 0000000..5181be6 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/kpFileName.ts @@ -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}`; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts index f58406b..d3f3a7e 100644 --- a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts +++ b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts @@ -1002,9 +1002,10 @@ export class CtService { 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 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( uploadUrl, @@ -1020,7 +1021,7 @@ export class CtService { if (!uploadRes.ok) throw new Error(`Ошибка загрузки файла: ${uploadRes.status}`); - const fileSRUrl = `${this.kpFolder}/${file.name}`; + const fileSRUrl = `${this.kpFolder}/${storageName}`; const itemRes = await this.http.get( `${this.baseUrl}/_api/web/GetFileByServerRelativeUrl('${encodeURIComponent(fileSRUrl)}')/ListItemAllFields?$select=Id`, SPHttpClient.configurations.v1, @@ -1028,7 +1029,7 @@ export class CtService { if (!itemRes.ok) throw new Error(`Ошибка получения ID файла: ${itemRes.status}`); 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 {