diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1df2e..db85282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,9 @@ Разделы: **Добавлено**, **Изменено**, **Исправлено**, **Удалено**. ## [Не выпущено] + +### Изменено + +- СТ ОМТС, Шаг 3: крестик у поставщика не удаляет его, а исключает из сравнения с обязательной + причиной. Цены сохраняются, под таблицей — список исключённых с кнопкой «Вернуть». Согласующим + список пока не показывается. (#1) diff --git a/docs/adr/0001-isklyuchenie-postavshchikov.md b/docs/adr/0001-isklyuchenie-postavshchikov.md index 5e38aa8..2bdca7a 100644 --- a/docs/adr/0001-isklyuchenie-postavshchikov.md +++ b/docs/adr/0001-isklyuchenie-postavshchikov.md @@ -2,7 +2,7 @@ - Статус: Принято - Дата: 2026-10-27 -- Задача: #1 +- Задача: #1 (СТ ОМТС); ОИД — #3, КПС — #4 - Источник: Асиля Сарниязова, Teams, 2026-10-23, п.4: «Я случайно удалила из таблицы поставщика — и не смогла вернуть его назад, хоть и есть КП» @@ -71,6 +71,20 @@ - Запись офферов и исключённых — в общем модуле для трёх форм, чтобы позже перенести сохранение на бэкенд без переделки компонентов. +## Устройство списков + +Манифесты — `provisioning/manifests/{ct,oid,kps}.provisioning.json`. + +- Колонки — копия «Оценки поставщиков» своей формы (чтобы «Вернуть» восстановило оффер + целиком) плюс `CT` (найти исключённых при открытии СТ) и `reason` (причина). +- Ссылки на обычные списки — Lookup, файл КП — сразу `offerFileId: Number`: лукап на + библиотеку документов не читается через REST. +- Номер строки комплекта — в `Title`, как в «Оценке поставщиков» (`поставщик-позиция-N`): + один формат во всех списках, разбор переиспользуется (`parseOfferItemIndex`). +- Причина повторяется в каждой строке поставщика — осознанная денормализация. Отдельный + список «Исключённые поставщики» дал бы 6 списков вместо 3 ради короткого текста. +- Поставщик без цен — строка без `position`, чтобы колонка не терялась. + ## Последствия - Три новых списка на `/sites/sed` — описать в `provisioning/manifests/{ct,oid,kps}`, @@ -79,6 +93,20 @@ - Попутно закрываются баги удаления: в ОИД выбор остаётся на удалённом поставщике, в КПС удалённый поставщик без КП возвращается после перезагрузки. +## Известные ограничения + +- **«Дополнительная информация» (условия оплаты, поставки, сроки) исключённого поставщика в SP + не пишется** — «Критерии» читает отчёт СТ, и исключённый попал бы в него. В сессии автора + условия сохраняются; если вернуть поставщика после открытия СТ в другом браузере, их придётся + заполнить заново. Цены возвращаются всегда. +- Если сессия автора уже есть, СТ берёт поставщиков из неё, а не из SP (как и раньше для + обычных поставщиков). + +## Объём + +Сейчас — только СТ ОМТС (#1). ОИД (#3) и КПС (#4) — позже: списки для них уже созданы, общий +модуль `shared/offers/excludedOffers.ts` готов к переиспользованию. + ## Ответы на открытые вопросы (2026-10-27, Тимур) 1. Согласующий исключённых — заложить, но пока не показывать. diff --git a/provisioning/Provision-Lists.ps1 b/provisioning/Provision-Lists.ps1 index 9c4129a..7411ee9 100644 --- a/provisioning/Provision-Lists.ps1 +++ b/provisioning/Provision-Lists.ps1 @@ -34,7 +34,13 @@ Тенант, например tnsukz.onmicrosoft.com .PARAMETER Thumbprint - Отпечаток сертификата в хранилище CurrentUser\My. + Отпечаток сертификата в хранилище CurrentUser\My. Либо он, либо CertificatePath. + +.PARAMETER CertificatePath + Путь к файлу сертификата .pfx — если сертификат не установлен в хранилище. + +.PARAMETER CertificatePassword + Пароль к .pfx из CertificatePath. .PARAMETER ManifestsDir Папка с манифестами. По умолчанию — provisioning/manifests рядом со скриптом. @@ -42,17 +48,30 @@ .PARAMETER Only Провижионить только манифесты указанных форм (по полю form). По умолчанию — все. +.PARAMETER Lists + Провижионить только списки с указанными названиями. Нужен для добавления нового списка + на рабочий сайт: полный прогон перезаписывает варианты Choice у живых списков + значениями из манифеста, а манифест мог отстать от живого списка. + .EXAMPLE ./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/sed" ` -ClientId "" -Tenant "tnsukz.onmicrosoft.com" -Thumbprint "" + +.EXAMPLE + ./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/sed" ` + -ClientId "" -Tenant "tnsukz.onmicrosoft.com" -Thumbprint "" ` + -Lists "Исключённые предложения", "Исключённые предложения ОИД" #> param( [Parameter(Mandatory = $true)][string]$SiteUrl, [Parameter(Mandatory = $true)][string]$ClientId, [Parameter(Mandatory = $true)][string]$Tenant, - [Parameter(Mandatory = $true)][string]$Thumbprint, + [string]$Thumbprint, + [string]$CertificatePath, + [string]$CertificatePassword, [string]$ManifestsDir = "$PSScriptRoot/manifests", - [string[]]$Only + [string[]]$Only, + [string[]]$Lists ) $ErrorActionPreference = 'Stop' @@ -143,7 +162,14 @@ function Add-ListColumn { } Write-Host "Подключение к $SiteUrl (app-only) ..." -ForegroundColor Cyan -Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -Thumbprint $Thumbprint +if ($Thumbprint) { + Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -Thumbprint $Thumbprint +} +elseif ($CertificatePath) { + $securePassword = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force + Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -CertificatePath $CertificatePath -CertificatePassword $securePassword +} +else { throw "Укажите -Thumbprint или -CertificatePath" } $files = Get-ChildItem -Path $ManifestsDir -Filter '*.provisioning.json' -ErrorAction SilentlyContinue if (-not $files) { throw "Манифесты *.provisioning.json не найдены в $ManifestsDir" } @@ -152,9 +178,18 @@ $manifests = @() foreach ($file in $files) { $def = Get-Content -Path $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json if ($Only -and ($Only -notcontains $def.form)) { continue } + if ($Lists) { + $def.lists = @($def.lists | Where-Object { $Lists -contains $_.title }) + if ($def.lists.Count -eq 0) { continue } + } $manifests += $def } -if (-not $manifests) { throw "Нет манифестов после фильтра -Only" } +if (-not $manifests) { throw "Нет манифестов после фильтров -Only / -Lists" } +if ($Lists) { + $found = @($manifests | ForEach-Object { $_.lists } | ForEach-Object { $_.title }) + $missing = @($Lists | Where-Object { $found -notcontains $_ }) + if ($missing.Count -gt 0) { throw "Списков нет в манифестах: $($missing -join ', ')" } +} # --- Проход A: создать все списки/библиотеки --- Write-Host "`n=== Проход A: списки ===" -ForegroundColor Cyan diff --git a/provisioning/manifests/ct.provisioning.json b/provisioning/manifests/ct.provisioning.json index a5a3a87..77cbe10 100644 --- a/provisioning/manifests/ct.provisioning.json +++ b/provisioning/manifests/ct.provisioning.json @@ -345,6 +345,97 @@ } ] }, + { + "title": "Исключённые предложения", + "template": "GenericList", + "shared": false, + "columns": [ + { + "internalName": "CT", + "title": "Номер СТ", + "type": "Lookup", + "lookupList": "Сравнительная таблица", + "lookupShowField": "number" + }, + { + "internalName": "supplier", + "title": "Поставщик", + "type": "Lookup", + "lookupList": "Поставщики", + "lookupShowField": "name" + }, + { + "internalName": "reason", + "title": "Причина исключения", + "type": "Note" + }, + { + "internalName": "position", + "title": "Позиция", + "type": "Lookup", + "lookupList": "Позиции СТ", + "lookupShowField": "position" + }, + { + "internalName": "supplierPosition", + "title": "Наименование позиции", + "type": "Text" + }, + { + "internalName": "unit", + "title": "Ед. изм", + "type": "Text" + }, + { + "internalName": "quantity", + "title": "Количество", + "type": "Number", + "decimals": 3 + }, + { + "internalName": "price", + "title": "Цена", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "summa", + "title": "Сумма", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "valuta", + "title": "Валюта", + "type": "Lookup", + "lookupList": "Курс Валют", + "lookupShowField": "code" + }, + { + "internalName": "priceTenge", + "title": "Цена в тенге", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "summaTenge", + "title": "Сумма в тенге", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "offerFileId", + "title": "Id файла КП", + "type": "Number", + "decimals": 0 + }, + { + "internalName": "deliveryDistanceKm", + "title": "Расстояние доставки, км", + "type": "Number" + } + ] + }, { "title": "Поставщики", "template": "GenericList", diff --git a/provisioning/manifests/kps.provisioning.json b/provisioning/manifests/kps.provisioning.json index 04c441f..d85d4db 100644 --- a/provisioning/manifests/kps.provisioning.json +++ b/provisioning/manifests/kps.provisioning.json @@ -386,6 +386,97 @@ "type": "Note" } ] + }, + { + "title": "Исключённые предложения КПС", + "template": "GenericList", + "shared": false, + "columns": [ + { + "internalName": "CT", + "title": "Номер СТ", + "type": "Lookup", + "lookupList": "Сравнительная таблица КПС", + "lookupShowField": "number" + }, + { + "internalName": "supplier", + "title": "Поставщик", + "type": "Lookup", + "lookupList": "Поставщики", + "lookupShowField": "name" + }, + { + "internalName": "reason", + "title": "Причина исключения", + "type": "Note" + }, + { + "internalName": "position", + "title": "Позиция", + "type": "Lookup", + "lookupList": "Позиции СТ КПС", + "lookupShowField": "position" + }, + { + "internalName": "supplierPosition", + "title": "Наименование позиции", + "type": "Text" + }, + { + "internalName": "unit", + "title": "Ед. изм", + "type": "Text" + }, + { + "internalName": "quantity", + "title": "Количество", + "type": "Number", + "decimals": 3 + }, + { + "internalName": "price", + "title": "Цена", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "summa", + "title": "Сумма", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "valuta", + "title": "Валюта", + "type": "Lookup", + "lookupList": "Курс Валют", + "lookupShowField": "code" + }, + { + "internalName": "priceTenge", + "title": "Цена в тенге", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "summaTenge", + "title": "Сумма в тенге", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "offerFileId", + "title": "Id файла КП", + "type": "Number", + "decimals": 0 + }, + { + "internalName": "deliveryDistanceKm", + "title": "Расстояние доставки, км", + "type": "Number" + } + ] } ] } diff --git a/provisioning/manifests/oid.provisioning.json b/provisioning/manifests/oid.provisioning.json index abcab3f..525d6ca 100644 --- a/provisioning/manifests/oid.provisioning.json +++ b/provisioning/manifests/oid.provisioning.json @@ -381,6 +381,73 @@ "type": "Boolean" } ] + }, + { + "title": "Исключённые предложения ОИД", + "template": "GenericList", + "shared": false, + "columns": [ + { + "internalName": "CT", + "title": "Номер СТ", + "type": "Lookup", + "lookupList": "Сравнительная таблица ОИД", + "lookupShowField": "number" + }, + { + "internalName": "supplier", + "title": "Поставщик", + "type": "Lookup", + "lookupList": "Поставщики", + "lookupShowField": "name" + }, + { + "internalName": "reason", + "title": "Причина исключения", + "type": "Note" + }, + { + "internalName": "position", + "title": "Позиция", + "type": "Lookup", + "lookupList": "Позиции СТ ОИД", + "lookupShowField": "position" + }, + { + "internalName": "supplierPosition", + "title": "Наименование позиции", + "type": "Text" + }, + { + "internalName": "unit", + "title": "Ед. изм", + "type": "Text" + }, + { + "internalName": "quantity", + "title": "Количество", + "type": "Number", + "decimals": 3 + }, + { + "internalName": "price", + "title": "Цена", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "summa", + "title": "Сумма", + "type": "Number", + "decimals": 2 + }, + { + "internalName": "offerFileId", + "title": "Id файла КП", + "type": "Number", + "decimals": 0 + } + ] } ] } diff --git a/provisioning/sources.json b/provisioning/sources.json index 7c4f5c5..ee4b67c 100644 --- a/provisioning/sources.json +++ b/provisioning/sources.json @@ -33,6 +33,7 @@ { "title": "Критерии", "template": "GenericList" }, { "title": "Согласование СТ", "template": "GenericList" }, { "title": "Не получены КП", "template": "GenericList" }, + { "title": "Исключённые предложения", "template": "GenericList" }, { "title": "Поставщики", "template": "GenericList" }, { "title": "Курс Валют", "template": "GenericList" }, { "title": "Матрица согласования СТ", "template": "GenericList" }, @@ -53,6 +54,7 @@ { "title": "Критерии ОИД", "template": "GenericList" }, { "title": "Согласование СТ ОИД", "template": "GenericList" }, { "title": "Не получены КП ОИД", "template": "GenericList" }, + { "title": "Исключённые предложения ОИД", "template": "GenericList" }, { "title": "Коммерческие предложения ОИД", "template": "DocumentLibrary" } ] } diff --git a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx index 60869e7..653a1df 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx @@ -11,6 +11,7 @@ import { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTabl import { Step4Approval } from './Step4Approval/Step4Approval'; import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns'; import { buildOffersFromSpRecords } from './utils/spOffers'; +import { excludedItemsToFormData } from './utils/excludedColumns'; import { isUserInApprovalChain } from './utils/approvalAccess'; import { CtAccess, getCtAccess, isCtAuthor } from './utils/ctAccess'; import { noOfferRecordsToSuppliers, withNoOfferSuppliers } from './utils/noOfferSuppliers'; @@ -82,6 +83,12 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, const preferSp = loadedAccess.isReview; const positionSpIds = positions.map(p => p.spId).filter((id): id is number => id !== undefined); const rawOffers = await service.loadFullOffersForPositions(positionSpIds); + // Исключённые из сравнения (docs/adr/0001). Не грузятся — СТ открываем без них, а не падаем: + // таблица сравнения важнее, но молча не глотаем. + const excludedItems = await service.loadExcludedOffers(initialCtId).catch(err => { + console.warn('[CtCreateForm] не удалось загрузить исключённых поставщиков:', err); + return []; + }); // Поставщики без КП этой СТ — для списка под таблицей сравнения (согласующий по ссылке). const noOfferFromSp = noOfferRecordsToSuppliers( await service.loadNoOfferSuppliers(initialCtId).catch(err => { @@ -103,7 +110,7 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, // Load currency rates for the valutaIds referenced by offers (separate query avoids // SP REST 400 that occurs when selecting a Choice field via $expand on a lookup). - const valutaIds = rawOffers + const valutaIds = [...rawOffers, ...excludedItems] .map(o => o.valutaSpId) .filter((id): id is number => id !== undefined && id > 0) .filter((id, idx, arr) => arr.indexOf(id) === idx); @@ -154,6 +161,15 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, currencyCodeOf, fallbackOfferFileId: supplierSpId => offerIdBySupplierSpId.get(supplierSpId), }); + // Исключённые — отдельными колонками с exclusion: Шаг 3 их не сравнивает, но может вернуть. + // Колонка есть и среди обычных — её вернули, а уборка прежних исключённых строк не прошла: + // обычная важнее, иначе колонка задвоится. + const allExcluded = excludedItemsToFormData(excludedItems, currencyCodeOf); + const activeColumnIds = new Set(spSuppliers.map(s => s.id)); + const excluded = { + suppliers: allExcluded.suppliers.filter(s => !activeColumnIds.has(s.id)), + offers: allExcluded.offers.filter(o => !activeColumnIds.has(o.supplierId)), + }; // Reconstruct uploadedKpFiles for users without session (other users opening CT via link) let uploadedKpFiles = formData.uploadedKpFiles; @@ -176,8 +192,11 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, ...(preferSp || formData.selectedPositions.length === 0 ? { selectedPositions: positions } : {}), // Preserve session suppliers and offers — SP data may be incomplete or stale vs. unsaved edits. // Only use SP-derived data when session has none, or in review mode (always fresh). - suppliers: withNoOfferSuppliers(formData.suppliers.length === 0 ? spSuppliers : formData.suppliers, noOfferFromSp), - ...(preferSp || formData.offers.length === 0 ? { offers } : {}), + suppliers: withNoOfferSuppliers( + formData.suppliers.length === 0 ? [...spSuppliers, ...excluded.suppliers] : formData.suppliers, + noOfferFromSp, + ), + ...(preferSp || formData.offers.length === 0 ? { offers: [...offers, ...excluded.offers] } : {}), ...(formData.currencyRates.length === 0 && spCurrencyRates.length > 0 ? { currencyRates: spCurrencyRates } : {}), ...(uploadedKpFiles.length > 0 ? { uploadedKpFiles } : {}), ...(Object.keys(step3DeliveryKm).length > 0 ? { step3DeliveryKm } : {}), @@ -196,7 +215,7 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, // Автор открыл свою СТ — открываем шаги, до которых она уже дошла (позиции → 2, КП → 3, маршрут → 4). ensureHighestStep(reachableStep({ hasPositions: positions.length > 0, - hasKp: rawOffers.length > 0 || uploadedKpFiles.length > 0, + hasKp: rawOffers.length > 0 || excludedItems.length > 0 || uploadedKpFiles.length > 0, hasRoute: approvalMatrix.length > 0, })); if (ct.status === 'approved' || ct.status === 'rejected') { diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/step2Persist.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/step2Persist.ts index fbbdb7b..2ff4b8b 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/step2Persist.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/step2Persist.ts @@ -4,6 +4,7 @@ import { CTFormData } from '../hooks/useCtForm'; import { KpFile, NoOfferEntry } from './kpTypes'; import { buildSupplierColumnId } from '../utils/supplierColumns'; import { getNoOfferSuppliers } from '../utils/noOfferSuppliers'; +import { keepExclusions } from '../utils/excludedColumns'; export interface PersistStep2Args { service: CtService; @@ -77,7 +78,8 @@ export async function persistStep2(args: PersistStep2Args): Promise ({ id: s.localId, @@ -94,7 +96,11 @@ export async function persistStep2(args: PersistStep2Args): Promise { const pos = formData.selectedPositions.find(p => p.id === o.positionId); - const supplierSpId = newSuppliersWithOffer.find(s => s.id === o.supplierId)?.spId; + const column = newSuppliersWithOffer.find(s => s.id === o.supplierId); + const supplierSpId = column?.spId; + // Исключённый поставщик не попадает в «Оценку поставщиков» — его строки пишет Шаг 3 + // в «Исключённые предложения». + if (column?.exclusion) return null; if (pos?.spId === undefined || supplierSpId === undefined) return null; return { ...o, positionId: String(pos.spId), supplierId: String(supplierSpId) }; }) diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts index 4244e7e..ae90a69 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts @@ -5,6 +5,7 @@ import { KpFile, FlowPositionResult, POLL_INTERVAL_MS, MAX_POLL_RETRIES } from ' import { buildKpSuppliersAndOffers } from './step2Persist'; import { buildKpOffersToSave } from './kpOfferRecords'; import { buildKpStorageName } from '../utils/kpFileName'; +import { keepExclusions } from '../utils/excludedColumns'; let localIdCounter = 0; @@ -62,7 +63,7 @@ export function useKpProcessing( // Заменяем колонки этих же КП (поставщик + файл), остальные — сохраняем: другой КП того же // поставщика — отдельное предложение, своя колонка. const others = (fd.suppliers ?? []).filter(s => !kpSuppliers.some(k => k.id === s.id)); - patch.suppliers = [...others, ...kpSuppliers]; + patch.suppliers = [...others, ...keepExclusions(kpSuppliers, fd.suppliers ?? [])]; const kpSupplierIds = new Set(kpSuppliers.map(s => s.id)); const otherOffers = (fd.offers ?? []).filter(o => !kpSupplierIds.has(o.supplierId)); patch.offers = [...otherOffers, ...kpOffers]; diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx index 930a10a..04743b7 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx @@ -7,6 +7,7 @@ import { AddExtraRow } from './AddExtraRow'; import { SUPPLIER_STATUS_OPTIONS, getSubCols, formatKzt } from './matrixHelpers'; import { UseAdataCheckResult } from './useAdataCheck'; import { AdataIndicator } from './AdataIndicator'; +import { ExcludeSupplierDialog } from './ExcludeSupplierDialog'; import styles from './Step3ComparisonTable.module.scss'; const FIXED_COLS = 3; // Наименование / Ед. изм. / Кол-во @@ -26,9 +27,13 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat const { isReadOnly, suppliersWithOffer, supplierCurrencies, selectedSupplierId, supplierStatuses, nameColWidth, regularPositions, extraPositionsRows, allPositions, computedOffers, - setSelectedSupplierId, handleDeleteSupplier, handleStatusChange, handleNameColResizeMouseDown, + setSelectedSupplierId, handleExcludeSupplier, handleStatusChange, handleNameColResizeMouseDown, } = matrix; + // Колонка, для которой открыто окно «Исключить из сравнения». + const [excludingSupplierId, setExcludingSupplierId] = React.useState(undefined); + const excludingSupplier = suppliersWithOffer.find(s => s.id === excludingSupplierId); + const totalColSpan = FIXED_COLS + suppliersWithOffer.reduce((sum, s) => sum + getSubCols(supplierCurrencies[s.id] ?? 'KZT', isRestrictedView), 0); // Высота row1 (шапка поставщика) переменная (теги валют, выбор поставщика, состояние Adata), @@ -115,8 +120,8 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat ) : ( @@ -257,6 +262,17 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat + + {excludingSupplier && ( + { + handleExcludeSupplier(excludingSupplier.id, reason); + setExcludingSupplierId(undefined); + }} + onCancel={() => setExcludingSupplierId(undefined)} + /> + )} ); } diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExcludeSupplierDialog.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExcludeSupplierDialog.tsx new file mode 100644 index 0000000..424c555 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExcludeSupplierDialog.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { Button, Dialog, TextArea } from '@tnsu/ui-kit'; + +interface Props { + supplierName: string; + onConfirm: (reason: string) => void; + onCancel: () => void; +} + +const REASON_ROWS = 3; + +// Исключение поставщика из сравнения (docs/adr/0001): причина обязательна — по ней потом видно, +// почему поставщик не рассматривался. +export const ExcludeSupplierDialog: React.FC = ({ supplierName, onConfirm, onCancel }) => { + const [reason, setReason] = useState(''); + const isReasonEmpty = reason.trim().length === 0; + + return ( + + + + + } + > +