From 2316f8bd6d8a55b8a72495671670073ce7b8c323 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: Wed, 30 Sep 2026 16:12:46 +0500 Subject: [PATCH] =?UTF-8?q?fix(ct):=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=9A=D0=9F=20=D0=BD=D0=B5=20=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=20=D0=B2=20SP=20=D0=B4?= =?UTF-8?q?=D1=83=D0=B1=D0=BB=D0=B8=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Каждая обработка КП создаёт записи в «Оценка поставщиков», а прежние удалялись только по «Сохранить». Исправленный КП поставщика или файл, загруженный заново, оставлял рядом старые записи — отсюда дубли в SP (СТ №89, №4). Теперь после создания записи прежние записи того же поставщика по той же позиции уходят в корзину; порядок «создать, потом удалить» — чтобы при сбое не потерять предложение. --- .../forms/ct/services/CtService.ts | 46 ++++++++++++++++++- .../shared/offers/offerRecords.test.ts | 39 +++++++++++++++- .../shared/offers/offerRecords.ts | 16 +++++++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts index adaa132..a4cc4d8 100644 --- a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts +++ b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts @@ -24,7 +24,11 @@ import { getRequestTypeIdByPageUrl, writeRoute, } from "../../../shared/approval"; -import { keepLatestOfferRecords, parseOfferItemIndex } from "../../../shared/offers/offerRecords"; +import { + findSupersededOfferIds, + keepLatestOfferRecords, + parseOfferItemIndex, +} from "../../../shared/offers/offerRecords"; // Round monetary values to 2 decimals before persisting to SharePoint (drops float tails). function round2(value: number): number { @@ -1598,6 +1602,46 @@ export class CtService { ); if (!res.ok) throw new Error(`Ошибка сохранения предложения: ${res.status}`); + + // Повторная обработка КП (исправленный КП, файл загружен заново) раньше добавляла записи рядом + // с прежними — в SP копились дубли, если потом не нажимали «Сохранить». Сначала создаём новую + // запись, потом убираем прежние: упади создание — старое предложение останется. + const created = (await res.json()) as { Id: number }; + await this._recycleSupersededOffers({ + id: created.Id, + supplierSpId: params.supplierSpId, + positionSpId: params.positionSpId, + }); + } + + private async _recycleSupersededOffers(saved: { + id: number; + supplierSpId: number; + positionSpId: number; + }): Promise { + const url = + `${this._li(LISTS.OFFERS)}?$select=Id,Title,positionId,supplierId` + + `&$filter=supplierId eq ${saved.supplierSpId} and positionId eq ${saved.positionSpId}`; + const res = await this.http.get(url, SPHttpClient.configurations.v1); + // Не критично для обработки КП: оставшиеся дубли отсеивают загрузчики (keepLatestOfferRecords), + // а «Сохранить» перезапишет записи позиции целиком. Но молча не глотаем. + if (!res.ok) { + console.warn(`[CtService] не удалось прочитать прежние предложения: ${res.status}`); + return; + } + const data = await res.json(); + const existing = ((data.value ?? []) as Array>).map((item) => ({ + id: Number(item.Id), + supplierSpId: Number(item.supplierId), + positionSpId: Number(item.positionId), + itemIndex: parseOfferItemIndex(String(item.Title ?? "")), + })); + // MERGE на этом списке блокирует ресивер — убираем через recycle(), как в saveOffers. + await this._runChunked(findSupersededOfferIds(existing, saved), 20, async (id) => { + await this.http + .post(`${this._liById(LISTS.OFFERS, id)}/recycle()`, SPHttpClient.configurations.v1, {}) + .catch(() => undefined /* уже в корзине / не найден — игнорируем */); + }); } // ─── Mapping ────────────────────────────────────────────────────────────── diff --git a/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts b/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts index bcb005f..bf09e2a 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, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords'; +import { IOfferRecordKey, findSupersededOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords'; interface ITestRecord extends IOfferRecordKey { price: number; @@ -57,3 +57,40 @@ describe('keepLatestOfferRecords', () => { expect(keepLatestOfferRecords(records)).toHaveLength(2); }); }); + +describe('findSupersededOfferIds', () => { + // Повторная обработка КП поставщика 259: новая запись 1850 заменяет прежнее предложение по позиции. + const saved = makeRecord({ id: 1850, positionSpId: 1478 }); + + it('возвращает прежние записи того же поставщика по той же позиции', () => { + const existing = [makeRecord({ id: 1836, positionSpId: 1478 }), saved]; + + expect(findSupersededOfferIds(existing, saved)).toEqual([1836]); + }); + + it('прежние строки комплекта тоже заменяются новым КП', () => { + const existing = [ + makeRecord({ id: 1836, positionSpId: 1478, itemIndex: 0 }), + makeRecord({ id: 1837, positionSpId: 1478, itemIndex: 1 }), + saved, + ]; + + expect(findSupersededOfferIds(existing, saved)).toEqual([1836, 1837]); + }); + + it('не трогает другие позиции и других поставщиков', () => { + const existing = [ + makeRecord({ id: 1836, positionSpId: 1476 }), + makeRecord({ id: 1837, positionSpId: 1478, supplierSpId: 233 }), + saved, + ]; + + expect(findSupersededOfferIds(existing, saved)).toEqual([]); + }); + + it('не трогает запись новее сохранённой (параллельная обработка)', () => { + const existing = [saved, makeRecord({ id: 1900, positionSpId: 1478 })]; + + expect(findSupersededOfferIds(existing, saved)).toEqual([]); + }); +}); diff --git a/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts b/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts index 2b3c076..700d2dc 100644 --- a/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts +++ b/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts @@ -30,3 +30,19 @@ export function keepLatestOfferRecords(records: T[]): } return Array.from(latestByKey.values()); } + +// Какие записи вытеснила только что сохранённая: прежние записи того же поставщика по той же позиции, +// включая строки комплекта — новый КП поставщика заменяет его прежнее предложение целиком. +// Записи новее сохранённой не трогаем: их могла создать параллельная обработка. +export function findSupersededOfferIds( + existing: IOfferRecordKey[], + saved: Pick, +): number[] { + return existing + .filter(record => + record.supplierSpId === saved.supplierSpId && + record.positionSpId === saved.positionSpId && + record.id < saved.id + ) + .map(record => record.id); +}