fix(ct): файл КП сохраняется с префиксом СТ — одноимённый КП другой СТ не перезаписывает его
КП загружались в корень библиотеки под исходным именем с перезаписью: «КП.pdf» одной СТ затирал «КП.pdf» другой, и согласующий первой СТ открывал чужое КП. Теперь файл хранится как «<id СТ>_<имя>» (buildKpStorageName); это имя идёт в поиск уже загруженного файла, поток, список «Загруженные КП» и ссылку на файл. Повторная загрузка того же файла в эту же СТ по-прежнему распознаётся.
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
+14
-8
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user