From 99c720f26ccfaab11208340abce6673649a598d4 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: Tue, 27 Oct 2026 17:35:53 +0500 Subject: [PATCH] =?UTF-8?q?feat(ct):=20=D1=81=D0=B2=D0=BE=D1=8F=20=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=B8=D1=86=D0=B0=20=D1=83=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8=20=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BF.=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BD=D0=B0=20=D0=A8=D0=B0=D0=B3=D0=B5=203=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit У логистики единица была зашита — «рейс», у доп. расходов её задавали только при добавлении. Теперь единица строки — поле ввода; поставщики, у которых её не меняли, получают новую, своя единица поставщика остаётся. Подсказка в количестве логистики — единица строки. Строка уже в SP — единица обновляется в «Позиции СТ» при сохранении. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Step3ComparisonTable/ExtraPositionRow.tsx | 23 +++++++++++--- .../matrixHelpers.test.ts | 31 +++++++++++++++++-- .../Step3ComparisonTable/matrixHelpers.ts | 19 ++++++++++++ .../Step3ComparisonTable/useStep3Matrix.ts | 17 +++++++++- .../forms/ct/services/CtService.ts | 15 +++++++++ 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx index fcae0cf..0eb1c98 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx @@ -25,7 +25,7 @@ export function ExtraPositionRow({ pos, posIndex, regularPositionsCount, matrix, isReadOnly, edits, extraPositions, translateUnits, rawValues, deliveryKm, selectedSupplierId, nameColWidth, suppliersWithOffer, supplierCurrencies, computedOffers, minTotalByPosition, updateSupplierProductName, updateUnit, updateEdit, updatePriceKzt, handleDeliveryKmChange, - handleDeleteExtraPosition, + handleDeleteExtraPosition, handleExtraPositionUnitChange, } = matrix; const ep = extraPositions.find(e => e.id === pos.id); @@ -47,7 +47,22 @@ export function ExtraPositionRow({ pos, posIndex, regularPositionsCount, matrix, /> )} - {pos.unit} + {/* Единица строки — своя (у логистики по умолчанию «рейс»: бывает «тонна», «машина», «услуга»). */} + + {!isReadOnly ? ( +
+ handleExtraPositionUnitChange(pos.id, val)} + placeholder="ед. изм." + borderless + ariaLabel={`Ед. изм. строки ${pos.materialName}`} + /> +
+ ) : ( + pos.unit + )} + — {suppliersWithOffer.map(s => { const key = key0(s); @@ -120,9 +135,9 @@ export function ExtraPositionRow({ pos, posIndex, regularPositionsCount, matrix, updateEdit(s.id, pos.id, 0, 'quantity', val)} - placeholder="рейсы" + placeholder={pos.unit || 'кол-во'} borderless - ariaLabel={`Кол-во рейсов ${s.name} ${pos.materialName}`} + ariaLabel={`Кол-во (${pos.unit || 'ед.'}) ${s.name} ${pos.materialName}`} /> ) : ( diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.test.ts index b220481..d3d6fdc 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.test.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.test.ts @@ -1,7 +1,34 @@ -import { applyCreatedIds, buildOfferKey, computeOffer, getSubCols, isMinPositionTotal, round2Str, sumPositionTotals } from './matrixHelpers'; -import { ExtraPosition } from './matrixTypes'; +import { applyCreatedIds, applyPositionUnitChange, buildOfferKey, computeOffer, getSubCols, isMinPositionTotal, round2Str, sumPositionTotals } from './matrixHelpers'; +import { EditableOffers, ExtraPosition } from './matrixTypes'; import { SupplierOffer } from '../types/domain'; +describe('applyPositionUnitChange (единица доп. позиции стала своей вместо «рейс»)', () => { + const edits: EditableOffers = { + 'sup_a::extra_pos_1': [{ supplierId: 'sup_a', positionId: 'extra_pos_1', itemIndex: 0, unit: 'рейс', pricePerUnit: 50000 }], + 'sup_b::extra_pos_1': [{ supplierId: 'sup_b', positionId: 'extra_pos_1', itemIndex: 0, unit: 'машина' }], + 'sup_c::extra_pos_1': [{ supplierId: 'sup_c', positionId: 'extra_pos_1', itemIndex: 0 }], + 'sup_a::pos_1': [{ supplierId: 'sup_a', positionId: 'pos_1', itemIndex: 0, unit: 'рейс' }], + }; + + const result = applyPositionUnitChange(edits, 'extra_pos_1', 'рейс', 'тонна'); + + it('у поставщика с прежней единицей позиции — новая, цена не теряется', () => { + expect(result['sup_a::extra_pos_1'][0]).toMatchObject({ unit: 'тонна', pricePerUnit: 50000 }); + }); + + it('у поставщика без своей единицы — новая', () => { + expect(result['sup_c::extra_pos_1'][0].unit).toBe('тонна'); + }); + + it('свою единицу поставщика не трогает', () => { + expect(result['sup_b::extra_pos_1'][0].unit).toBe('машина'); + }); + + it('другие позиции не трогает, даже с той же единицей', () => { + expect(result['sup_a::pos_1'][0].unit).toBe('рейс'); + }); +}); + describe('computeOffer', () => { it('считает сумму в тенге для КП в тенге', () => { const offer = computeOffer( diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.ts index 3b0a650..008ee67 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/matrixHelpers.ts @@ -87,6 +87,25 @@ export function buildOfferKey(supplierId: string, positionId: string): string { return `${supplierId}::${positionId}`; } +// Поменяли единицу доп. позиции (логистика, доп. расходы): у поставщиков, у которых единица та же, +// что была у позиции (её не трогали), — новая. Свою единицу поставщика не меняем. Ячейки без правок +// и так показывают единицу позиции. +export function applyPositionUnitChange( + edits: EditableOffers, + positionId: string, + oldUnit: string, + newUnit: string, +): EditableOffers { + const next: EditableOffers = {}; + Object.keys(edits).forEach(key => { + const isThisPosition = key.split('::')[1] === positionId; + next[key] = isThisPosition + ? edits[key].map(item => (!item.unit || item.unit === oldUnit ? { ...item, unit: newUnit } : item)) + : edits[key]; + }); + return next; +} + export interface ColumnOffersInput { columns: Array<{ id: string; isExtraSupplier: boolean }>; positions: PositionRow[]; diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts index c4cdbd3..9d0435f 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts @@ -4,7 +4,7 @@ import { SupplierOffer, CurrencyRate, FormStep, SupplierExclusion } from '../typ import { CtService } from '../../services/CtService'; import { CTFormData } from '../hooks/useCtForm'; import { ExtraSupplier, ExtraPosition, PositionRow, CellEdit, EditableOffers, CriteriaEntry } from './matrixTypes'; -import { applyCreatedIds, buildOfferKey, computeColumnsOffers, sumPositionTotals } from './matrixHelpers'; +import { applyCreatedIds, applyPositionUnitChange, buildOfferKey, computeColumnsOffers, sumPositionTotals } from './matrixHelpers'; import { buildExcludedRecordsForSave } from '../utils/excludedColumns'; import { parseNumberInput } from '../utils/parseNumberInput'; import { getStep3SaveError } from './step3Validation'; @@ -681,6 +681,17 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) { onSave({ step3DeliveryKm: updated }); }; + // Своя единица доп. позиции вместо заданной при добавлении (у логистики — «рейс»). Поставщики, у + // которых единицу не меняли, получают новую. В SP уходит при «Сохранить» (см. handleSave). + const handleExtraPositionUnitChange = (positionId: string, unit: string): void => { + if (isReadOnly) return; + const oldUnit = extraPositions.find(ep => ep.id === positionId)?.unit ?? ''; + const updated = extraPositions.map(ep => (ep.id === positionId ? { ...ep, unit } : ep)); + setExtraPositions(updated); + setEdits(prev => applyPositionUnitChange(prev, positionId, oldUnit, unit)); + onSave({ step3ExtraPositions: updated }); + }; + const handleDeleteExtraPosition = (positionId: string): void => { if (isReadOnly) return; const updated = extraPositions.filter(ep => ep.id !== positionId); @@ -760,6 +771,9 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) { if (ep.spId) { // Already created in SP on a previous save — reuse to avoid duplicate Позиции СТ rows. extraPosSpIds[ep.id] = ep.spId; + // Единицу могли поменять после создания. Пишем всегда — строк доп. расходов единицы, + // а отслеживать «менялась ли» — лишнее состояние. + await service.updateCTPositionUnit(ep.spId, ep.unit); continue; } const saved = await service.createCTPositions(ct.id, [{ @@ -992,6 +1006,7 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) { handleAddDelivery, handleDeliveryKmChange, handleDeleteExtraPosition, + handleExtraPositionUnitChange, validateBeforeSave, handleSave, draftSavedAt, diff --git a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts index 68e900a..0497124 100644 --- a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts +++ b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts @@ -397,6 +397,21 @@ export class CtService { })); } + // Единица доп. позиции Шага 3 (логистика, доп. расходы) — её можно поменять после создания. + // MERGE на «Позиции СТ» ресивер не блокирует (блокирует только на «Оценка поставщиков»). + async updateCTPositionUnit(positionSpId: number, unit: string): Promise { + const res = await this.http.fetch(this._liById(LISTS.CT_POSITIONS, positionSpId), SPHttpClient.configurations.v1, { + method: "POST", + headers: { + "Content-Type": "application/json;odata=nometadata", + "IF-MATCH": "*", + "X-HTTP-Method": "MERGE", + }, + body: JSON.stringify({ unit }), + }); + if (!res.ok) throw new Error(`Ошибка обновления единицы позиции: ${res.status}`); + } + // Пакетное создание элементов списка через SharePoint OData $batch: вместо N POST — // один HTTP-запрос (до 100 операций/changeset, при большем числе бьём на чанки). // Возвращает Id созданных элементов В ТОМ ЖЕ порядке, что и bodies. Changeset атомарен: