diff --git a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx index 1e87c39..53c4a40 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx @@ -9,6 +9,7 @@ import { Step2CommercialOffers } from './Step2CommercialOffers/Step2CommercialOf import { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTable'; import { Step4Approval } from './Step4Approval/Step4Approval'; import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns'; +import { buildOffersFromSpRecords } from './utils/spOffers'; import styles from './CtCreateForm.module.scss'; import '@tnsu/ui-kit/styles.css'; @@ -124,23 +125,10 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, // Build offers in domain format. Файл КП берём из самой записи (offerFileId) — карта из сессии // знает один файл на поставщика и при нескольких КП отдала бы обеим колонкам один и тот же. - const offers = rawOffers.map(o => { - const offerIdNum = o.offerFileSpId ?? offerIdBySupplierSpId.get(o.supplierSpId); - const currencyCode = o.valutaSpId ? (rateById.get(o.valutaSpId)?.code ?? 'KZT') : 'KZT'; - return { - id: '', - supplierId: columnIdOf(o.supplierSpId, o.offerFileSpId), - positionId: String(o.positionSpId), - itemIndex: o.itemIndex, - unit: o.unit, - pricePerUnit: o.price, - quantity: o.quantity, - currencyCode, - pricePerUnitKzt: o.priceTenge || o.price, - totalPriceKzt: o.summaTenge, - totalWithExtraKzt: o.summaTenge, - offerId: offerIdNum !== undefined ? String(offerIdNum) : undefined, - }; + const offers = buildOffersFromSpRecords(rawOffers, { + columnIdOf, + currencyCodeOf, + fallbackOfferFileId: supplierSpId => offerIdBySupplierSpId.get(supplierSpId), }); // Reconstruct uploadedKpFiles for users without session (other users opening CT via link) diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.test.ts new file mode 100644 index 0000000..a489fe3 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.test.ts @@ -0,0 +1,66 @@ +import { SpOfferRecord, buildOffersFromSpRecords } from './spOffers'; + +function makeRecord(overrides: Partial): SpOfferRecord { + return { + positionSpId: 1543, + supplierSpId: 263, + supplierName: 'ТОО «Арлан Офис Снаб»', + supplierPosition: 'Доска пробковая в раме 90х120', + price: 28500, + quantity: 4, + unit: 'шт.', + priceTenge: 28500, + summaTenge: 114000, + currencyCode: 'KZT', + itemIndex: 0, + offerFileSpId: 277, + ...overrides, + }; +} + +const deps = { + columnIdOf: (supplierSpId: number, offerFileSpId?: number): string => `${supplierSpId}_${offerFileSpId}`, + currencyCodeOf: (): string => 'KZT', + fallbackOfferFileId: (): number | undefined => undefined, +}; + +describe('buildOffersFromSpRecords (открытие СТ по ссылке / без сессии)', () => { + // Раньше название из КП терялось: шаг 3 подставлял название из заявки, а «Сохранить» записывало + // его в SP поверх названия поставщика — согласующий не видел, что предложена другая модель. + it('сохраняет наименование ТМЦ из КП', () => { + const [offer] = buildOffersFromSpRecords([makeRecord({})], deps); + + expect(offer.supplierPosition).toBe('Доска пробковая в раме 90х120'); + }); + + it('пустое наименование из КП не подменяет названием из заявки', () => { + const [offer] = buildOffersFromSpRecords([makeRecord({ supplierPosition: '' })], deps); + + expect(offer.supplierPosition).toBeUndefined(); + }); + + it('переносит колонку, позицию, цены и файл КП', () => { + const [offer] = buildOffersFromSpRecords([makeRecord({})], deps); + + expect(offer).toMatchObject({ + supplierId: '263_277', + positionId: '1543', + itemIndex: 0, + unit: 'шт.', + pricePerUnit: 28500, + quantity: 4, + pricePerUnitKzt: 28500, + totalPriceKzt: 114000, + offerId: '277', + }); + }); + + it('без файла в записи берёт файл КП из сессии', () => { + const [offer] = buildOffersFromSpRecords( + [makeRecord({ offerFileSpId: undefined })], + { ...deps, fallbackOfferFileId: () => 300 }, + ); + + expect(offer.offerId).toBe('300'); + }); +}); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.ts new file mode 100644 index 0000000..af87ce8 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.ts @@ -0,0 +1,49 @@ +import { SupplierOffer } from '../types/domain'; + +// Запись «Оценка поставщиков», прочитанная из SP (CtService.loadFullOffersForPositions). +export interface SpOfferRecord { + positionSpId: number; + supplierSpId: number; + supplierName: string; + supplierPosition: string; // наименование ТМЦ в КП поставщика + price: number; + quantity: number; + unit: string; + priceTenge: number; + summaTenge: number; + currencyCode: string; + itemIndex: number; + valutaSpId?: number; + offerFileSpId?: number; +} + +export interface BuildOffersDeps { + columnIdOf: (supplierSpId: number, offerFileSpId?: number) => string; + currencyCodeOf: (valutaSpId?: number) => string; + // Файл КП из сессии — для старых записей без offerFileId. + fallbackOfferFileId: (supplierSpId: number) => number | undefined; +} + +// Офферы формы из записей SP (открытие СТ по ссылке / без сессии). Наименование ТМЦ из КП переносим +// обязательно: без него шаг 3 подставляет название из заявки — согласующий не видит, что поставщик +// предложил другую модель, а «Сохранить» записывает название заявки в SP поверх названия поставщика. +export function buildOffersFromSpRecords(records: SpOfferRecord[], deps: BuildOffersDeps): SupplierOffer[] { + return records.map(r => { + const offerFileId = r.offerFileSpId ?? deps.fallbackOfferFileId(r.supplierSpId); + return { + id: '', + supplierId: deps.columnIdOf(r.supplierSpId, r.offerFileSpId), + positionId: String(r.positionSpId), + itemIndex: r.itemIndex, + supplierPosition: r.supplierPosition || undefined, + unit: r.unit, + pricePerUnit: r.price, + quantity: r.quantity, + currencyCode: deps.currencyCodeOf(r.valutaSpId), + pricePerUnitKzt: r.priceTenge || r.price, + totalPriceKzt: r.summaTenge, + totalWithExtraKzt: r.summaTenge, + offerId: offerFileId !== undefined ? String(offerFileId) : undefined, + }; + }); +} diff --git a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts index 52e8059..74d8a69 100644 --- a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts +++ b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts @@ -779,6 +779,7 @@ export class CtService { positionSpId: number; supplierSpId: number; supplierName: string; + supplierPosition: string; price: number; quantity: number; unit: string; @@ -799,7 +800,7 @@ export class CtService { // valutaId is selected directly; caller enriches currencyCode via loadCurrencyRatesByIds. const url = `${this._li(LISTS.OFFERS)}` + - `?$select=Id,Title,positionId,supplierId,price,quantity,unit,priceTenge,summaTenge,valutaId,deliveryDistanceKm,offerFileId,supplier/Id,supplier/Title` + + `?$select=Id,Title,positionId,supplierId,supplierPosition,price,quantity,unit,priceTenge,summaTenge,valutaId,deliveryDistanceKm,offerFileId,supplier/Id,supplier/Title` + `&$expand=supplier` + `&$filter=${filter}` + `&$top=2000`; @@ -816,6 +817,7 @@ export class CtService { positionSpId: Number(item["positionId"]), supplierSpId: Number(item["supplierId"]), supplierName: String(sup?.["Title"] ?? ""), + supplierPosition: String(item["supplierPosition"] ?? ""), price: Number(item["price"] ?? 0), quantity: Number(item["quantity"] ?? 0), unit: String(item["unit"] ?? ""),