diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/KpReplaceDialog.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/KpReplaceDialog.tsx new file mode 100644 index 0000000..704c702 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/KpReplaceDialog.tsx @@ -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 = ({ question, onAnswer }) => ( + onAnswer('separate')} + width="md" + footer={ + <> + + + + } + > +

Уже загружено:

+ +

+ «Заменить» — прежний КП уберётся из сравнения, останется новый (исправленный КП, тот же файл повторно). + «Добавить отдельным предложением» — оба КП будут отдельными колонками (напр. другая валюта или вариант). +

+
+); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.module.scss b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.module.scss index 27a549b..857a978 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.module.scss +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.module.scss @@ -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; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx index 066e5d9..b2e74c5 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx @@ -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 = ({ disabled={isNotificationLink || isReadOnly} /> {kp.duplicateWarning && {kp.duplicateWarning}} + {kp.replaceQuestion && ( + + )} {kp.isProcessing && ( Файлы обрабатываются системой Syntex. Можно продолжить заполнение формы. )} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpReplace.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpReplace.test.ts new file mode 100644 index 0000000..60da763 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpReplace.test.ts @@ -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]); + }); +}); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpReplace.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpReplace.ts new file mode 100644 index 0000000..6c25ad0 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpReplace.ts @@ -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([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; + +// «Заменить»: убираем прежние колонки поставщика (все, кроме колонки нового файла КП), их предложения +// и записи в списке «Загруженные КП». Сам файл остаётся в библиотеке — удаляются только данные СТ. +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)), + }; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts index a48b386..24aeece 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts @@ -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([]); + 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 => { + 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 => { + 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): 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, }; } diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpReplaceQuestion.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpReplaceQuestion.ts new file mode 100644 index 0000000..aac510e --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpReplaceQuestion.ts @@ -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; + answer: (choice: KpReplaceChoice) => void; +} + +// Очередь вопросов: несколько КП обрабатываются параллельно, а окно одно — спрашиваем по очереди. +// Обработка КП ждёт ответа (Promise) перед записью предложений в SP. +// Ушли с шага, не ответив — считаем «добавить отдельно»: так ничего не удаляется. +export function useKpReplaceQuestion(): UseKpReplaceQuestionResult { + const queueRef = useRef([]); + const [current, setCurrent] = useState(undefined); + + const showNext = useCallback((): void => { + setCurrent(queueRef.current[0]?.question); + }, []); + + const ask = useCallback((question: KpReplaceQuestion): Promise => { + return new Promise(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 }; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts index 4df8065..e44e9b3 100644 --- a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts +++ b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts @@ -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 { + 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>).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; diff --git a/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts b/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts index 3edc1ba..823e8d3 100644 --- a/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts +++ b/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts @@ -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]); + }); +}); diff --git a/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts b/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts index 2bc785f..6a7fc5e 100644 --- a/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts +++ b/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts @@ -46,6 +46,18 @@ export function findSupplierOfferIds( .map(record => record.id); } +// «Заменить» прежний КП поставщика новым: все записи поставщика, кроме записей нового файла КП, +// включая старые записи без файла. +export function findOtherKpOfferIds( + records: Array>, + supplierSpId: number, + keepFileSpId: number, +): number[] { + return records + .filter(record => record.supplierSpId === supplierSpId && record.offerFileSpId !== keepFileSpId) + .map(record => record.id); +} + // Какие записи вытеснила только что сохранённая: прежние записи того же поставщика и того же файла КП // по той же позиции, включая строки комплекта. Записи другого КП того же поставщика — отдельное // предложение, их не трогаем. Записи новее сохранённой тоже: их могла создать параллельная обработка.