feat(ct): выбор «заменить / добавить отдельно» при втором КП того же поставщика
Система не отличает исправленный КП от отдельного предложения поставщика: поставщик определяется только по названию. Теперь, когда поток назвал поставщика, у которого в СТ уже есть КП, обработка ждёт выбора в окне. «Заменить» — прежние колонки поставщика уходят из сравнения, их записи в SP — в корзину (после записи нового КП, чтобы не потерять данные при сбое). «Добавить отдельным предложением» — отдельная колонка. Закрытие окна — «добавить»: ничего не удаляется. Несколько КП подряд — вопросы по очереди. Этап 2 из 4 (п.2).
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Dialog } from '@tnsu/ui-kit';
|
||||
import { KpReplaceChoice } from './kpReplace';
|
||||
import { KpReplaceQuestion } from './useKpReplaceQuestion';
|
||||
import styles from './Step2CommercialOffers.module.scss';
|
||||
|
||||
interface Props {
|
||||
question: KpReplaceQuestion;
|
||||
onAnswer: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
// Окно выбора для второго КП того же поставщика. Закрытие крестиком/Esc = «Добавить отдельно»:
|
||||
// безопасный вариант, ничего не удаляет.
|
||||
export const KpReplaceDialog: React.FC<Props> = ({ question, onAnswer }) => (
|
||||
<Dialog
|
||||
open
|
||||
title="У поставщика уже есть КП"
|
||||
subText={`${question.supplierName}: загружен ещё один КП — ${question.fileName}`}
|
||||
onDismiss={() => onAnswer('separate')}
|
||||
width="md"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" onClick={() => onAnswer('separate')}>
|
||||
Добавить отдельным предложением
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => onAnswer('replace')}>
|
||||
Заменить
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>Уже загружено:</p>
|
||||
<ul className={styles.replaceExistingList}>
|
||||
{question.existing.map(f => <li key={f.spId ?? f.name}>{f.name}</li>)}
|
||||
</ul>
|
||||
<p className={styles.replaceHint}>
|
||||
«Заменить» — прежний КП уберётся из сравнения, останется новый (исправленный КП, тот же файл повторно).
|
||||
«Добавить отдельным предложением» — оба КП будут отдельными колонками (напр. другая валюта или вариант).
|
||||
</p>
|
||||
</Dialog>
|
||||
);
|
||||
+13
@@ -206,3 +206,16 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// Окно «У поставщика уже есть КП» (KpReplaceDialog).
|
||||
.replaceExistingList {
|
||||
margin: 4px 0 12px;
|
||||
padding-left: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.replaceHint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
+4
@@ -11,6 +11,7 @@ import { useKpProcessing } from './useKpProcessing';
|
||||
import { persistStep2 } from './step2Persist';
|
||||
import { SuppliersWithoutKp } from './SuppliersWithoutKp';
|
||||
import { UploadedKpList } from './UploadedKpList';
|
||||
import { KpReplaceDialog } from './KpReplaceDialog';
|
||||
import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency';
|
||||
import styles from './Step2CommercialOffers.module.scss';
|
||||
|
||||
@@ -135,6 +136,9 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
||||
disabled={isNotificationLink || isReadOnly}
|
||||
/>
|
||||
{kp.duplicateWarning && <Alert variant="warn">{kp.duplicateWarning}</Alert>}
|
||||
{kp.replaceQuestion && (
|
||||
<KpReplaceDialog question={kp.replaceQuestion} onAnswer={kp.answerReplaceQuestion} />
|
||||
)}
|
||||
{kp.isProcessing && (
|
||||
<Alert variant="info">Файлы обрабатываются системой Syntex. Можно продолжить заполнение формы.</Alert>
|
||||
)}
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { findOtherKpFilesOfSupplier, removeReplacedColumns } from './kpReplace';
|
||||
import { Supplier, SupplierOffer } from '../types/domain';
|
||||
|
||||
const ARLAN = 'ТОО «Арлан Офис Снаб»';
|
||||
const KZT_COLUMN: Supplier = { id: '263_277', spId: 263, kpFileSpId: 277, name: ARLAN, hasOffer: true };
|
||||
const KALA_COLUMN: Supplier = { id: '264_278', spId: 264, kpFileSpId: 278, name: 'ТОО «Кала Саплай»', hasOffer: true };
|
||||
const UPLOADED = [
|
||||
{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf', supplierName: ARLAN },
|
||||
{ spId: 278, name: 'KP_02_Qala_Supply_KZT.pdf', supplierName: 'ТОО «Кала Саплай»' },
|
||||
];
|
||||
|
||||
function makeOffer(supplierId: string): SupplierOffer {
|
||||
return {
|
||||
id: '', supplierId, positionId: 'p1', pricePerUnit: 1, quantity: 1, currencyCode: 'KZT',
|
||||
pricePerUnitKzt: 1, totalPriceKzt: 1, totalWithExtraKzt: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe('findOtherKpFilesOfSupplier', () => {
|
||||
// Пришёл второй КП Арлан (файл 285) — спросить, заменить ли им KP_01_Arlan_Office_KZT.pdf.
|
||||
it('находит уже загруженный КП того же поставщика', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [KZT_COLUMN, KALA_COLUMN],
|
||||
uploadedKpFiles: UPLOADED,
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf' }]);
|
||||
});
|
||||
|
||||
it('первый КП поставщика — спрашивать нечего', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [KALA_COLUMN],
|
||||
uploadedKpFiles: UPLOADED,
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([]);
|
||||
});
|
||||
|
||||
it('тот же файл, обработанный повторно, не считается другим КП', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [KZT_COLUMN],
|
||||
uploadedKpFiles: UPLOADED,
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 277,
|
||||
});
|
||||
|
||||
expect(other).toEqual([]);
|
||||
});
|
||||
|
||||
it('видит КП того же поставщика, который ещё обрабатывается в этой сессии', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [],
|
||||
uploadedKpFiles: [],
|
||||
sessionFiles: [{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf', supplierSpId: 263 }],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf' }]);
|
||||
});
|
||||
|
||||
it('старая колонка без файла КП тоже считается загруженным КП', () => {
|
||||
const legacy: Supplier = { id: '263', spId: 263, name: ARLAN, hasOffer: true };
|
||||
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [legacy],
|
||||
uploadedKpFiles: [],
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([{ spId: undefined, name: ARLAN }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeReplacedColumns', () => {
|
||||
it('«Заменить» убирает прежние колонки поставщика, их предложения и файлы', () => {
|
||||
const usdColumn: Supplier = { id: '263_285', spId: 263, kpFileSpId: 285, name: ARLAN, hasOffer: true };
|
||||
|
||||
const result = removeReplacedColumns(
|
||||
{
|
||||
suppliers: [KZT_COLUMN, KALA_COLUMN, usdColumn],
|
||||
offers: [makeOffer('263_277'), makeOffer('264_278'), makeOffer('263_285')],
|
||||
uploadedKpFiles: [...UPLOADED, { spId: 285, name: 'KP_01_Arlan_Office_USD.pdf', supplierName: ARLAN }],
|
||||
},
|
||||
263,
|
||||
285,
|
||||
);
|
||||
|
||||
expect(result.suppliers.map(s => s.id)).toEqual(['264_278', '263_285']);
|
||||
expect(result.offers.map(o => o.supplierId)).toEqual(['264_278', '263_285']);
|
||||
expect(result.uploadedKpFiles.map(f => f.spId)).toEqual([278, 285]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Supplier, SupplierOffer } from '../types/domain';
|
||||
import { CTFormData } from '../hooks/useCtForm';
|
||||
|
||||
// Второй КП того же поставщика: пользователь выбирает, заменить им прежний (исправленный КП, тот же
|
||||
// файл под другим именем) или добавить отдельным предложением (напр. другая валюта). Сама система
|
||||
// эти случаи не различает — поставщик определяется только по названию.
|
||||
export type KpReplaceChoice = 'replace' | 'separate';
|
||||
|
||||
// Уже загруженный КП поставщика — для текста вопроса. spId нет у старых колонок без файла КП.
|
||||
export interface ExistingKpFile {
|
||||
spId?: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FindOtherKpFilesArgs {
|
||||
columns: Supplier[];
|
||||
uploadedKpFiles: CTFormData['uploadedKpFiles'];
|
||||
// КП этой сессии, которые ещё обрабатываются: их колонки появятся только после обработки.
|
||||
sessionFiles: Array<{ spId?: number; name: string; supplierSpId?: number }>;
|
||||
supplierSpId: number;
|
||||
fileSpId: number;
|
||||
}
|
||||
|
||||
// Другие КП того же поставщика в этой СТ (кроме только что обработанного файла).
|
||||
export function findOtherKpFilesOfSupplier(args: FindOtherKpFilesArgs): ExistingKpFile[] {
|
||||
const { columns, uploadedKpFiles, sessionFiles, supplierSpId, fileSpId } = args;
|
||||
const found: ExistingKpFile[] = [];
|
||||
const seenFileIds = new Set<number>([fileSpId]);
|
||||
|
||||
columns
|
||||
.filter(c => c.hasOffer && c.spId === supplierSpId)
|
||||
.forEach(c => {
|
||||
if (c.kpFileSpId === undefined) {
|
||||
// Старая колонка без файла КП (данные до появления нескольких КП у поставщика).
|
||||
found.push({ spId: undefined, name: c.name });
|
||||
return;
|
||||
}
|
||||
if (seenFileIds.has(c.kpFileSpId)) return;
|
||||
seenFileIds.add(c.kpFileSpId);
|
||||
const uploaded = uploadedKpFiles.find(u => u.spId === c.kpFileSpId);
|
||||
found.push({ spId: c.kpFileSpId, name: uploaded?.name ?? c.name });
|
||||
});
|
||||
|
||||
sessionFiles
|
||||
.filter(f => f.supplierSpId === supplierSpId && f.spId !== undefined && !seenFileIds.has(f.spId))
|
||||
.forEach(f => {
|
||||
seenFileIds.add(f.spId as number);
|
||||
found.push({ spId: f.spId, name: f.name });
|
||||
});
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
type ReplaceableData = Pick<CTFormData, 'suppliers' | 'offers' | 'uploadedKpFiles'>;
|
||||
|
||||
// «Заменить»: убираем прежние колонки поставщика (все, кроме колонки нового файла КП), их предложения
|
||||
// и записи в списке «Загруженные КП». Сам файл остаётся в библиотеке — удаляются только данные СТ.
|
||||
export function removeReplacedColumns(
|
||||
data: ReplaceableData,
|
||||
supplierSpId: number,
|
||||
keepFileSpId: number,
|
||||
): ReplaceableData {
|
||||
const isReplaced = (s: Supplier): boolean =>
|
||||
s.hasOffer === true && s.spId === supplierSpId && s.kpFileSpId !== keepFileSpId;
|
||||
|
||||
const replacedColumns = data.suppliers.filter(isReplaced);
|
||||
const replacedColumnIds = new Set(replacedColumns.map(s => s.id));
|
||||
const replacedFileIds = new Set(
|
||||
replacedColumns.map(s => s.kpFileSpId).filter((id): id is number => id !== undefined),
|
||||
);
|
||||
|
||||
return {
|
||||
suppliers: data.suppliers.filter(s => !isReplaced(s)),
|
||||
offers: data.offers.filter((o: SupplierOffer) => !replacedColumnIds.has(o.supplierId)),
|
||||
uploadedKpFiles: data.uploadedKpFiles.filter(f => !replacedFileIds.has(f.spId)),
|
||||
};
|
||||
}
|
||||
+63
@@ -4,6 +4,8 @@ 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 { KpReplaceChoice, findOtherKpFilesOfSupplier, removeReplacedColumns } from './kpReplace';
|
||||
import { KpReplaceQuestion, useKpReplaceQuestion } from './useKpReplaceQuestion';
|
||||
|
||||
let localIdCounter = 0;
|
||||
|
||||
@@ -15,6 +17,9 @@ export interface UseKpProcessingResult {
|
||||
handleFileRemove: (localId: string) => void;
|
||||
handleCurrencyChange: (localId: string, currency: string) => void;
|
||||
isProcessing: boolean;
|
||||
// Вопрос «заменить / добавить отдельно» для второго КП поставщика (undefined — окна нет).
|
||||
replaceQuestion: KpReplaceQuestion | undefined;
|
||||
answerReplaceQuestion: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
// Конвейер обработки КП (Шаг 2): загрузка в библиотеку → извлечение текста (Word) → поллинг Syntex →
|
||||
@@ -31,6 +36,7 @@ export function useKpProcessing(
|
||||
const mountedRef = useRef(true);
|
||||
const formDataRef = useRef(formData);
|
||||
const kpFilesRef = useRef<KpFile[]>([]);
|
||||
const replaceQuestion = useKpReplaceQuestion();
|
||||
|
||||
useEffect(() => { formDataRef.current = formData; }, [formData]);
|
||||
useEffect(() => { kpFilesRef.current = kpFiles; }, [kpFiles]);
|
||||
@@ -69,6 +75,50 @@ export function useKpProcessing(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kpFiles]);
|
||||
|
||||
// Другие КП этого поставщика в СТ — если есть, спрашиваем «заменить или добавить отдельно».
|
||||
const askReplaceChoice = async (
|
||||
localId: string,
|
||||
fileName: string,
|
||||
fileSpId: number,
|
||||
supplierSpId: number,
|
||||
supplierName: string,
|
||||
): Promise<KpReplaceChoice> => {
|
||||
const fd = formDataRef.current;
|
||||
const existing = findOtherKpFilesOfSupplier({
|
||||
columns: fd.suppliers ?? [],
|
||||
uploadedKpFiles: fd.uploadedKpFiles ?? [],
|
||||
sessionFiles: kpFilesRef.current
|
||||
.filter(f => f.localId !== localId && f.status !== 'error')
|
||||
.map(f => ({ spId: f.spId, name: f.file.name, supplierSpId: f.supplierSpId })),
|
||||
supplierSpId,
|
||||
fileSpId,
|
||||
});
|
||||
if (existing.length === 0) return 'separate';
|
||||
return replaceQuestion.ask({ fileName, supplierName, existing });
|
||||
};
|
||||
|
||||
// «Заменить»: прежние КП поставщика уходят из сравнения — из стейта сразу, из SP (в корзину) после
|
||||
// записи предложений нового КП. Стейт обновляем до статуса done нового файла: иначе инкрементальный
|
||||
// persist ниже успеет собрать колонки по ещё не очищенным formData.
|
||||
const applyReplace = async (supplierSpId: number, keepFileSpId: number): Promise<void> => {
|
||||
const fd = formDataRef.current;
|
||||
setKpFiles(prev => prev.filter(f => f.supplierSpId !== supplierSpId || f.spId === keepFileSpId));
|
||||
onSave(removeReplacedColumns(
|
||||
{ suppliers: fd.suppliers ?? [], offers: fd.offers ?? [], uploadedKpFiles: fd.uploadedKpFiles ?? [] },
|
||||
supplierSpId,
|
||||
keepFileSpId,
|
||||
));
|
||||
const positionSpIds = [...fd.selectedPositions, ...(fd.step3ExtraPositions ?? [])]
|
||||
.map(p => p.spId)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
try {
|
||||
await service.recycleOtherKpOffers(supplierSpId, positionSpIds, keepFileSpId);
|
||||
} catch (err) {
|
||||
// Не критично: «Сохранить» на Шаге 2/3 перезапишет предложения позиций по текущей таблице.
|
||||
console.warn('[КП] не удалось убрать предложения прежнего КП:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const patchFile = (localId: string, patch: Partial<KpFile>): void => {
|
||||
if (!mountedRef.current) return;
|
||||
setKpFiles(prev => prev.map(f => f.localId === localId ? { ...f, ...patch } : f));
|
||||
@@ -107,9 +157,11 @@ export function useKpProcessing(
|
||||
|
||||
// If supplier wasn't set by Syntex, use supplier name from flow result
|
||||
let resolvedSupplierSpId = supplierSpId;
|
||||
let resolvedSupplierName = kpFilesRef.current.find(f => f.localId === localId)?.supplierName ?? '';
|
||||
if (!resolvedSupplierSpId) {
|
||||
const supplierName = collectedResults.find(r => r.flowResult.supplier)?.flowResult.supplier;
|
||||
if (supplierName) {
|
||||
resolvedSupplierName = supplierName;
|
||||
try {
|
||||
resolvedSupplierSpId = await service.saveSupplierWithOffer(supplierName);
|
||||
patchFile(localId, { supplierName, supplierSpId: resolvedSupplierSpId });
|
||||
@@ -119,6 +171,11 @@ export function useKpProcessing(
|
||||
}
|
||||
}
|
||||
|
||||
// Второй КП того же поставщика — ждём выбора пользователя до записи в SP.
|
||||
const replaceChoice: KpReplaceChoice = resolvedSupplierSpId
|
||||
? await askReplaceChoice(localId, fileName, fileSpId, resolvedSupplierSpId, resolvedSupplierName)
|
||||
: 'separate';
|
||||
|
||||
// Предложения пишем после того, как поставщик определён: Syntex отключён, и поставщика чаще
|
||||
// называет сам поток — внутри цикла по позициям его ещё нет, и записи раньше не создавались вовсе.
|
||||
const offersToSave = buildKpOffersToSave(collectedResults, positions, resolvedSupplierSpId, fileSpId);
|
||||
@@ -129,6 +186,10 @@ export function useKpProcessing(
|
||||
})
|
||||
));
|
||||
|
||||
if (replaceChoice === 'replace' && resolvedSupplierSpId) {
|
||||
await applyReplace(resolvedSupplierSpId, fileSpId);
|
||||
}
|
||||
|
||||
// БИН поставщика (для Adata) — берём первый результат из позиций, где он вообще пришёл
|
||||
const supplierBIN = collectedResults.find(r => r.flowResult.supplierBIN)?.flowResult.supplierBIN;
|
||||
if (supplierBIN && resolvedSupplierSpId) {
|
||||
@@ -285,5 +346,7 @@ export function useKpProcessing(
|
||||
handleFileRemove,
|
||||
handleCurrencyChange,
|
||||
isProcessing,
|
||||
replaceQuestion: replaceQuestion.current,
|
||||
answerReplaceQuestion: replaceQuestion.answer,
|
||||
};
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ExistingKpFile, KpReplaceChoice } from './kpReplace';
|
||||
|
||||
// Вопрос «заменить или добавить отдельно» по одному обработанному КП.
|
||||
export interface KpReplaceQuestion {
|
||||
fileName: string;
|
||||
supplierName: string;
|
||||
existing: ExistingKpFile[];
|
||||
}
|
||||
|
||||
interface PendingQuestion {
|
||||
question: KpReplaceQuestion;
|
||||
resolve: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
export interface UseKpReplaceQuestionResult {
|
||||
// Вопрос, который сейчас показан (undefined — окна нет).
|
||||
current: KpReplaceQuestion | undefined;
|
||||
ask: (question: KpReplaceQuestion) => Promise<KpReplaceChoice>;
|
||||
answer: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
// Очередь вопросов: несколько КП обрабатываются параллельно, а окно одно — спрашиваем по очереди.
|
||||
// Обработка КП ждёт ответа (Promise) перед записью предложений в SP.
|
||||
// Ушли с шага, не ответив — считаем «добавить отдельно»: так ничего не удаляется.
|
||||
export function useKpReplaceQuestion(): UseKpReplaceQuestionResult {
|
||||
const queueRef = useRef<PendingQuestion[]>([]);
|
||||
const [current, setCurrent] = useState<KpReplaceQuestion | undefined>(undefined);
|
||||
|
||||
const showNext = useCallback((): void => {
|
||||
setCurrent(queueRef.current[0]?.question);
|
||||
}, []);
|
||||
|
||||
const ask = useCallback((question: KpReplaceQuestion): Promise<KpReplaceChoice> => {
|
||||
return new Promise<KpReplaceChoice>(resolve => {
|
||||
queueRef.current.push({ question, resolve });
|
||||
if (queueRef.current.length === 1) showNext();
|
||||
});
|
||||
}, [showNext]);
|
||||
|
||||
const answer = useCallback((choice: KpReplaceChoice): void => {
|
||||
const pending = queueRef.current.shift();
|
||||
pending?.resolve(choice);
|
||||
showNext();
|
||||
}, [showNext]);
|
||||
|
||||
useEffect(() => () => {
|
||||
queueRef.current.forEach(p => p.resolve('separate'));
|
||||
queueRef.current = [];
|
||||
}, []);
|
||||
|
||||
return { current, ask, answer };
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
writeRoute,
|
||||
} from "../../../shared/approval";
|
||||
import {
|
||||
findOtherKpOfferIds,
|
||||
findSupersededOfferIds,
|
||||
keepLatestOfferRecords,
|
||||
parseOfferItemIndex,
|
||||
@@ -1618,6 +1619,35 @@ export class CtService {
|
||||
});
|
||||
}
|
||||
|
||||
// «Заменить» прежний КП поставщика новым (Шаг 2): записи поставщика по позициям СТ, кроме записей
|
||||
// нового файла, уходят в корзину. Вызывается после записи предложений нового КП — при сбое записи
|
||||
// прежнее предложение останется.
|
||||
async recycleOtherKpOffers(
|
||||
supplierSpId: number,
|
||||
positionSpIds: number[],
|
||||
keepFileSpId: number,
|
||||
): Promise<void> {
|
||||
if (positionSpIds.length === 0) return;
|
||||
const positionFilter = positionSpIds.map((id) => `positionId eq ${id}`).join(" or ");
|
||||
const url =
|
||||
`${this._li(LISTS.OFFERS)}?$select=Id,supplierId,offerFileId` +
|
||||
`&$filter=supplierId eq ${supplierSpId} and (${positionFilter})&$top=5000`;
|
||||
const res = await this.http.get(url, SPHttpClient.configurations.v1);
|
||||
if (!res.ok) throw new Error(`Ошибка чтения прежних предложений: ${res.status}`);
|
||||
const data = await res.json();
|
||||
const records = ((data.value ?? []) as Array<Record<string, unknown>>).map((item) => ({
|
||||
id: Number(item.Id),
|
||||
supplierSpId: Number(item.supplierId),
|
||||
offerFileSpId: item.offerFileId ? Number(item.offerFileId) : undefined,
|
||||
}));
|
||||
// MERGE на этом списке блокирует ресивер — убираем через recycle(), как в saveOffers.
|
||||
await this._runChunked(findOtherKpOfferIds(records, supplierSpId, keepFileSpId), 20, async (id) => {
|
||||
await this.http
|
||||
.post(`${this._liById(LISTS.OFFERS, id)}/recycle()`, SPHttpClient.configurations.v1, {})
|
||||
.catch(() => undefined /* уже в корзине / не найден — игнорируем */);
|
||||
});
|
||||
}
|
||||
|
||||
private async _recycleSupersededOffers(saved: {
|
||||
id: number;
|
||||
supplierSpId: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IOfferRecordKey, findSupersededOfferIds, findSupplierOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
||||
import { IOfferRecordKey, findOtherKpOfferIds, findSupersededOfferIds, findSupplierOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
||||
|
||||
interface ITestRecord extends IOfferRecordKey {
|
||||
price: number;
|
||||
@@ -149,3 +149,16 @@ describe('несколько КП одного поставщика (offerFileSp
|
||||
expect(findSupersededOfferIds(existing, saved)).toEqual([15]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOtherKpOfferIds («Заменить» прежний КП новым)', () => {
|
||||
it('возвращает записи других КП поставщика и старые записи без файла', () => {
|
||||
const records = [
|
||||
makeRecord({ id: 10, supplierSpId: 263, offerFileSpId: 277 }),
|
||||
makeRecord({ id: 11, supplierSpId: 263 }),
|
||||
makeRecord({ id: 12, supplierSpId: 263, offerFileSpId: 285 }),
|
||||
makeRecord({ id: 13, supplierSpId: 264, offerFileSpId: 278 }),
|
||||
];
|
||||
|
||||
expect(findOtherKpOfferIds(records, 263, 285)).toEqual([10, 11]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +46,18 @@ export function findSupplierOfferIds(
|
||||
.map(record => record.id);
|
||||
}
|
||||
|
||||
// «Заменить» прежний КП поставщика новым: все записи поставщика, кроме записей нового файла КП,
|
||||
// включая старые записи без файла.
|
||||
export function findOtherKpOfferIds(
|
||||
records: Array<Pick<IOfferRecordKey, 'id' | 'supplierSpId' | 'offerFileSpId'>>,
|
||||
supplierSpId: number,
|
||||
keepFileSpId: number,
|
||||
): number[] {
|
||||
return records
|
||||
.filter(record => record.supplierSpId === supplierSpId && record.offerFileSpId !== keepFileSpId)
|
||||
.map(record => record.id);
|
||||
}
|
||||
|
||||
// Какие записи вытеснила только что сохранённая: прежние записи того же поставщика и того же файла КП
|
||||
// по той же позиции, включая строки комплекта. Записи другого КП того же поставщика — отдельное
|
||||
// предложение, их не трогаем. Записи новее сохранённой тоже: их могла создать параллельная обработка.
|
||||
|
||||
Reference in New Issue
Block a user