fix(ct): «Дополнительная информация» сохраняется в SP; списки для оплаты, поставки, сертификатов

Ручные правки условий и свои строки («Добавить условие») жили только в браузере: saveCriteria
не вызывался, и согласующий, PDF и автор в новой вкладке видели лишь то, что распознал ИИ.
Теперь по «Сохранить» шага 3 «Критерии» этой СТ заменяются текущими (replaceCriteria), а при
открытии грузятся по СТ, а не по поставщику из всех СТ (loadCriteriaForCt). Свои строки
хранятся в новой колонке criteriaLabel, значение — в comment, как в ОИД.

По просьбе заказчика: «Условия оплаты» — готовые варианты (частые формулировки из SP) или свой
вариант; «Сертификаты» — Да/Нет; «Условия поставки» можно сменить и после заполнения ИИ
(раньше заполненное показывалось текстом без выбора).
This commit is contained in:
Тимур Городецкий
2026-10-01 16:18:02 +05:00
parent f5853b1ad5
commit 2b102b8772
9 changed files with 405 additions and 78 deletions
@@ -255,6 +255,11 @@
"type": "Lookup",
"lookupList": "Поставщики",
"lookupShowField": "name"
},
{
"internalName": "criteriaLabel",
"title": "Название доп.условия",
"type": "Text"
}
]
},
@@ -0,0 +1,33 @@
import * as React from 'react';
import styles from './Step3ComparisonTable.module.scss';
export interface CriteriaChoiceOption {
value: string;
label: string;
}
interface Props {
value: string;
options: CriteriaChoiceOption[];
onChange: (value: string) => void;
ariaLabel: string;
}
// Ячейка-выбор «Дополнительной информации» (условия поставки, сертификаты). Значение не из списка
// (напр. свободный текст от ИИ) показываем отдельным пунктом — чтобы не потерялось и его можно было сменить.
export const CriteriaChoiceCell: React.FC<Props> = ({ value, options, onChange, ariaLabel }) => {
const current = value.trim();
const isKnown = current === '' || options.some(o => o.value === current);
return (
<select
className={styles.criteriaSelect}
value={current}
onChange={e => onChange(e.target.value)}
aria-label={ariaLabel}
>
<option value="">—</option>
{!isKnown && <option value={current}>{current}</option>}
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
);
};
@@ -2,13 +2,21 @@ import * as React from 'react';
import { Button, IconButton, TextInput, TextArea, Icon } from '@tnsu/ui-kit';
import { Step3Matrix } from './useStep3Matrix';
import { CriteriaEntry } from './matrixTypes';
import { DELIVERY_TYPE_OPTIONS } from './matrixHelpers';
import { CERTIFICATE_OPTIONS, DELIVERY_TYPE_OPTIONS } from './matrixHelpers';
import { CriteriaChoiceCell, CriteriaChoiceOption } from './CriteriaChoiceCell';
import { PaymentTypeCell } from './PaymentTypeCell';
import styles from './Step3ComparisonTable.module.scss';
interface Props {
matrix: Step3Matrix;
}
// Поля-выбор: условия поставки (Incoterms — в списке описание, храним код) и сертификаты Да/Нет.
const CHOICE_OPTIONS: Partial<Record<keyof CriteriaEntry, CriteriaChoiceOption[]>> = {
deliveryType: DELIVERY_TYPE_OPTIONS.map(o => ({ value: o.code, label: o.label })),
sertificates: CERTIFICATE_OPTIONS.map(o => ({ value: o, label: o })),
};
// Блок «Дополнительная информация» (условия оплаты/поставки/сроки/сертификаты + произвольные условия).
// Перенесено 1:1 из Step3ComparisonTable (§стайл-гайд: чистая вёрстка, логика — в useStep3Matrix).
export function CriteriaTable({ matrix }: Props): JSX.Element | null {
@@ -54,24 +62,22 @@ export function CriteriaTable({ matrix }: Props): JSX.Element | null {
return (
<td key={s.id} className={styles.criteriaValue}>
{!isReadOnly ? (
row.key === 'deliveryType' ? (
(criteriaMap[s.id]?.deliveryType ?? '').trim() ? (
// Уже заполнено (в т.ч. ИИ) — показываем как текст, без селекта.
criteriaMap[s.id]?.deliveryType
) : (
// Пусто — даём выбрать условие поставки из списка.
<select
className={styles.criteriaSelect}
value=""
onChange={e => updateCriteriaValue(s.id, 'deliveryType', e.target.value)}
aria-label={`${row.label} — ${s.name}`}
>
<option value="">—</option>
{DELIVERY_TYPE_OPTIONS.map(o => (
<option key={o.code} value={o.code}>{o.label}</option>
))}
</select>
)
CHOICE_OPTIONS[row.key] ? (
// Выбор из списка — всегда, даже если поле уже заполнено (в т.ч. ИИ): раньше
// заполненное условие поставки показывалось текстом и его нельзя было сменить.
<CriteriaChoiceCell
value={criteriaMap[s.id]?.[row.key] ?? ''}
options={CHOICE_OPTIONS[row.key] ?? []}
onChange={val => updateCriteriaValue(s.id, row.key, val)}
ariaLabel={`${row.label} — ${s.name}`}
/>
) : row.key === 'paymentType' ? (
<PaymentTypeCell
value={criteriaMap[s.id]?.paymentType ?? ''}
onChange={val => updateCriteriaValue(s.id, 'paymentType', val)}
ariaLabel={`${row.label} — ${s.name}`}
isRequiredEmpty={Boolean(row.required) && isEmpty}
/>
) : (
<div className={[styles.criteriaInput, row.required && isEmpty ? styles.criteriaInputRequired : ''].filter(Boolean).join(' ')}>
<TextArea
@@ -0,0 +1,59 @@
import * as React from 'react';
import { useState } from 'react';
import { TextArea } from '@tnsu/ui-kit';
import { PAYMENT_TYPE_OPTIONS } from './matrixHelpers';
import { isPaymentTypeOption } from '../utils/criteria';
import styles from './Step3ComparisonTable.module.scss';
const CUSTOM_OPTION = '__custom';
interface Props {
value: string;
onChange: (value: string) => void;
ariaLabel: string;
isRequiredEmpty: boolean;
}
// Условия оплаты: готовые варианты или «Свой вариант…» с полем ввода. Значение не из списка
// (вписанное вручную или распознанное ИИ) открывается сразу как свой вариант.
export const PaymentTypeCell: React.FC<Props> = ({ value, onChange, ariaLabel, isRequiredEmpty }) => {
// Выбран «Свой вариант…» вручную (поле ещё пустое) — или значение уже не из списка. Второе считаем
// от значения, а не один раз при монтировании: условия из SP приходят уже после первого рендера.
const [isCustomChosen, setIsCustomChosen] = useState(false);
const isCustom = isCustomChosen || (value.trim() !== '' && !isPaymentTypeOption(value));
const handleSelect = (selected: string): void => {
if (selected === CUSTOM_OPTION) {
setIsCustomChosen(true);
if (isPaymentTypeOption(value)) onChange('');
return;
}
setIsCustomChosen(false);
onChange(selected);
};
return (
<div className={[styles.criteriaInput, isRequiredEmpty ? styles.criteriaInputRequired : ''].filter(Boolean).join(' ')}>
<select
className={styles.criteriaSelect}
value={isCustom ? CUSTOM_OPTION : value.trim()}
onChange={e => handleSelect(e.target.value)}
aria-label={ariaLabel}
>
<option value="">—</option>
{PAYMENT_TYPE_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)}
<option value={CUSTOM_OPTION}>Свой вариант…</option>
</select>
{isCustom && (
<TextArea
value={value}
onChange={onChange}
placeholder="Впишите условия оплаты"
borderless
rows={1}
ariaLabel={`${ariaLabel} — свой вариант`}
/>
)}
</div>
);
};
@@ -21,6 +21,19 @@ export const DELIVERY_TYPE_OPTIONS: Array<{ code: string; label: string }> = [
{ code: 'DDP', label: 'Поставка до объекта с оплатой всех расходов (DDP) — доставка силами поставщика с включением транспортных и таможенных расходов' },
];
// Условия оплаты — готовые варианты (самые частые формулировки в SP «Критерии»); можно вписать свой.
export const PAYMENT_TYPE_OPTIONS: string[] = [
'100% предоплата',
'50% предоплата, 50% по факту поставки',
'30% предоплата, 70% по факту поставки',
'40% предоплата, 60% перед отгрузкой',
'Оплата по факту поставки',
'100% оплата в течение 5 рабочих дней после поставки',
];
// Сертификаты — как Choice-колонка sertificates в SP «Критерии».
export const CERTIFICATE_OPTIONS: string[] = ['Да', 'Нет'];
// Foreign currency = 6 sub-cols (Ед. изм. | Кол-во | Цена_curr | Сумма_curr | Цена ₸ | Сумма ₸)
// KZT = 4 sub-cols (Ед. изм. | Кол-во | Цена ₸ | Сумма ₸)
// Restricted = 3 sub-cols (Наименование ТМЦ | Ед. изм. | Кол-во)
@@ -8,6 +8,7 @@ import { applyCreatedIds, buildOfferKey, computeOffer } from './matrixHelpers';
import { IVendorRecord } from '../../services/VendorDatabaseService';
import { buildColumnDisplayNames, buildColumnLabels, findColumnForRecord } from '../utils/supplierColumns';
import { getNoOfferSuppliers } from '../utils/noOfferSuppliers';
import { buildCriteriaRecords, restoreCriteria } from '../utils/criteria';
let extraIdCounter = 0;
@@ -131,27 +132,18 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
const criteriaMapRef = useRef(criteriaMap);
const customCriteriaRef = useRef(customCriteria);
// Load criteria from SP "Критерии" list by supplier SP IDs
// «Дополнительная информация» из SP «Критерии» этой СТ: условия поставщиков и свои строки. Правки
// из сессии важнее — SP подставляем только туда, где в сессии пусто.
React.useEffect(() => {
const supplierSpIds = suppliers
.filter(s => s.hasOffer && s.spId !== undefined)
.map(s => s.spId as number);
if (supplierSpIds.length === 0) return;
service.loadCriteria(supplierSpIds)
.then(rawCriteria => {
const spMap: Record<string, CriteriaEntry> = {};
// Критерии в SP — на поставщика, а не на КП: у всех колонок поставщика (несколько КП) они общие.
rawCriteria.forEach(c => {
suppliers
.filter(s => s.spId === c.supplierSpId)
.forEach(sup => {
spMap[sup.id] = { paymentType: c.paymentType, deliveryType: c.deliveryType, deadline: c.deadline, sertificates: c.sertificates };
});
});
// Merge SP data with any user edits from formData — user edits take priority
setCriteriaMap(prev => ({ ...spMap, ...prev }));
if (!ct?.id) return;
const columns = suppliers.filter(s => s.hasOffer !== false);
service.loadCriteriaForCt(ct.id)
.then(records => {
const restored = restoreCriteria(records, columns);
setCriteriaMap(prev => ({ ...restored.criteriaMap, ...prev }));
setCustomCriteria(prev => (prev.length > 0 ? prev : restored.customCriteria));
})
.catch(() => undefined);
.catch(err => console.warn('[Шаг 3] не удалось загрузить доп. информацию:', err));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -0,0 +1,98 @@
import { buildCriteriaRecords, isPaymentTypeOption, restoreCriteria } from './criteria';
import { Supplier } from '../types/domain';
const ARLAN_KZT: Supplier = { id: '263_277', spId: 263, kpFileSpId: 277, name: 'ТОО «Арлан Офис Снаб»', hasOffer: true };
const ARLAN_USD: Supplier = { id: '263_285', spId: 263, kpFileSpId: 285, name: 'ТОО «Арлан Офис Снаб»', hasOffer: true };
const KALA: Supplier = { id: '264_278', spId: 264, kpFileSpId: 278, name: 'ТОО «Кала Саплай»', hasOffer: true };
const MANUAL: Supplier = { id: 'extra_sup_1', name: 'Ручной поставщик', hasOffer: true };
const spIdOf = (column: Supplier): number | undefined => column.spId ?? (column.id === 'extra_sup_1' ? 300 : undefined);
describe('buildCriteriaRecords (ручные правки «Дополнительной информации» → SP)', () => {
// Раньше правки и свои строки жили только в браузере: согласующий и PDF видели лишь то, что распознал ИИ.
it('одна запись условий на поставщика', () => {
const records = buildCriteriaRecords(
[KALA],
{ '264_278': { paymentType: '100% предоплата', deliveryType: 'EXW', deadline: '10 дней', sertificates: 'Да' } },
[],
spIdOf,
);
expect(records).toEqual([{
supplierSpId: 264, paymentType: '100% предоплата', deliveryType: 'EXW', deadline: '10 дней', sertificates: 'Да',
}]);
});
// Условия в SP — на поставщика, а не на КП: у колонок одного поставщика берём первую заполненную.
it('несколько КП одного поставщика — одна запись', () => {
const records = buildCriteriaRecords(
[ARLAN_KZT, ARLAN_USD],
{
'263_277': { paymentType: '', deliveryType: '', deadline: '', sertificates: '' },
'263_285': { paymentType: '50% предоплата, 50% по факту поставки', deliveryType: '', deadline: '', sertificates: '' },
},
[],
spIdOf,
);
expect(records).toHaveLength(1);
expect(records[0]).toMatchObject({ paymentType: '50% предоплата, 50% по факту поставки' });
});
it('свои строки — запись на (строка, поставщик) с непустым значением', () => {
const records = buildCriteriaRecords(
[KALA, MANUAL],
{},
[{ id: 'c1', label: 'Гарантия', values: { '264_278': '12 мес', extra_sup_1: '' } }],
spIdOf,
);
// Пустые условия поставщика не пишем — только заполненная своя строка.
expect(records).toEqual([{ supplierSpId: 264, criteriaLabel: 'Гарантия', value: '12 мес' }]);
});
it('пропускает колонку без SP Id поставщика', () => {
const noSp: Supplier = { id: 'local_1', name: 'Без SP', hasOffer: true };
expect(buildCriteriaRecords([noSp], { local_1: { paymentType: 'x', deliveryType: '', deadline: '', sertificates: '' } }, [], () => undefined)).toEqual([]);
});
});
describe('restoreCriteria (SP → таблица, в т.ч. по ссылке)', () => {
it('условия поставщика расходятся по всем его колонкам, свои строки группируются по названию', () => {
const restored = restoreCriteria(
[
{ id: 1, supplierSpId: 263, paymentType: 'Отсрочка', deliveryType: 'DAP', deadline: '', sertificates: 'Нет' },
{ id: 2, supplierSpId: 263, criteriaLabel: 'Гарантия', value: '24 мес' },
{ id: 3, supplierSpId: 264, criteriaLabel: 'Гарантия', value: '12 мес' },
],
[ARLAN_KZT, ARLAN_USD, KALA],
);
expect(restored.criteriaMap['263_277'].paymentType).toBe('Отсрочка');
expect(restored.criteriaMap['263_285'].sertificates).toBe('Нет');
expect(restored.customCriteria).toEqual([
{ id: 'sp_Гарантия', label: 'Гарантия', values: { '263_277': '24 мес', '263_285': '24 мес', '264_278': '12 мес' } },
]);
});
// ИИ при каждой обработке КП добавляет запись условий — берём последнюю.
it('из нескольких записей условий поставщика берёт последнюю', () => {
const restored = restoreCriteria(
[
{ id: 5, supplierSpId: 264, paymentType: 'старое', deliveryType: '', deadline: '', sertificates: '' },
{ id: 9, supplierSpId: 264, paymentType: 'новое', deliveryType: '', deadline: '', sertificates: '' },
],
[KALA],
);
expect(restored.criteriaMap['264_278'].paymentType).toBe('новое');
});
});
describe('isPaymentTypeOption', () => {
it('значение из списка — да, своё — нет', () => {
expect(isPaymentTypeOption('100% предоплата')).toBe(true);
expect(isPaymentTypeOption('40% предоплата, 60% по аккредитиву')).toBe(false);
});
});
@@ -0,0 +1,117 @@
import { Supplier } from '../types/domain';
import { CriteriaEntry } from '../Step3ComparisonTable/matrixTypes';
import { PAYMENT_TYPE_OPTIONS } from '../Step3ComparisonTable/matrixHelpers';
// Своя строка «Дополнительной информации» (кнопка «Добавить условие»): значения по колонкам.
export interface CustomCriteriaRow {
id: string;
label: string;
values: Record<string, string>;
}
// Запись «Критерии» в SP. Условия поставщика — одна запись на поставщика; своя строка — запись на
// (строка, поставщик): название в criteriaLabel, значение в comment (как в «Критерии ОИД»).
export interface FixedCriteriaRecord {
id?: number;
supplierSpId: number;
paymentType: string;
deliveryType: string;
deadline: string;
sertificates: string;
}
export interface CustomCriteriaRecord {
id?: number;
supplierSpId: number;
criteriaLabel: string;
value: string;
}
export type CriteriaRecord = FixedCriteriaRecord | CustomCriteriaRecord;
const isCustomRecord = (r: CriteriaRecord): r is CustomCriteriaRecord => 'criteriaLabel' in r;
const EMPTY_ENTRY: CriteriaEntry = { paymentType: '', deliveryType: '', deadline: '', sertificates: '' };
const hasAnyValue = (e: CriteriaEntry): boolean =>
[e.paymentType, e.deliveryType, e.deadline, e.sertificates].some(v => (v ?? '').trim() !== '');
export function isPaymentTypeOption(value: string): boolean {
return PAYMENT_TYPE_OPTIONS.includes(value.trim());
}
// Таблица «Дополнительная информация» → записи SP. Условия в SP — на поставщика, а не на КП: у колонок
// одного поставщика (несколько КП) берём первую заполненную. Колонки без SP Id поставщика пропускаем.
export function buildCriteriaRecords(
columns: Supplier[],
criteriaMap: Record<string, CriteriaEntry>,
customCriteria: CustomCriteriaRow[],
spIdOf: (column: Supplier) => number | undefined,
): CriteriaRecord[] {
const records: CriteriaRecord[] = [];
const savedSuppliers = new Set<number>();
columns.forEach(column => {
const supplierSpId = spIdOf(column);
const entry = criteriaMap[column.id];
if (supplierSpId === undefined || savedSuppliers.has(supplierSpId) || !entry || !hasAnyValue(entry)) return;
savedSuppliers.add(supplierSpId);
records.push({
supplierSpId,
paymentType: entry.paymentType.trim(),
deliveryType: entry.deliveryType.trim(),
deadline: entry.deadline.trim(),
sertificates: entry.sertificates.trim(),
});
});
customCriteria.forEach(row => {
const savedForRow = new Set<number>();
columns.forEach(column => {
const supplierSpId = spIdOf(column);
const value = (row.values[column.id] ?? '').trim();
if (supplierSpId === undefined || !value || savedForRow.has(supplierSpId)) return;
savedForRow.add(supplierSpId);
records.push({ supplierSpId, criteriaLabel: row.label, value });
});
});
return records;
}
// Записи SP → таблица (в т.ч. при открытии по ссылке). Условия поставщика расходятся по всем его
// колонкам; ИИ при каждой обработке КП добавляет запись — берём последнюю (max Id).
export function restoreCriteria(
records: CriteriaRecord[],
columns: Supplier[],
): { criteriaMap: Record<string, CriteriaEntry>; customCriteria: CustomCriteriaRow[] } {
const latestFixed = new Map<number, FixedCriteriaRecord>();
records.forEach(r => {
if (isCustomRecord(r)) return;
const previous = latestFixed.get(r.supplierSpId);
if (!previous || (r.id ?? 0) > (previous.id ?? 0)) latestFixed.set(r.supplierSpId, r);
});
const criteriaMap: Record<string, CriteriaEntry> = {};
columns.forEach(column => {
const fixed = column.spId !== undefined ? latestFixed.get(column.spId) : undefined;
if (!fixed) return;
criteriaMap[column.id] = {
...EMPTY_ENTRY,
paymentType: fixed.paymentType,
deliveryType: fixed.deliveryType,
deadline: fixed.deadline,
sertificates: fixed.sertificates,
};
});
const customByLabel = new Map<string, CustomCriteriaRow>();
records.forEach(r => {
if (!isCustomRecord(r)) return;
const row = customByLabel.get(r.criteriaLabel) ?? { id: `sp_${r.criteriaLabel}`, label: r.criteriaLabel, values: {} };
columns.filter(c => c.spId === r.supplierSpId).forEach(c => { row.values[c.id] = r.value; });
customByLabel.set(r.criteriaLabel, row);
});
return { criteriaMap, customCriteria: Array.from(customByLabel.values()) };
}
@@ -8,7 +8,6 @@ import {
Request,
Supplier,
SupplierOffer,
SupplierCriteria,
SyntexFields,
FlowResult,
AdataCompanyInfo,
@@ -30,6 +29,7 @@ import {
keepLatestOfferRecords,
parseOfferItemIndex,
} from "../../../shared/offers/offerRecords";
import { CriteriaRecord } from "../components/utils/criteria";
import {
NoOfferSupplierEntry,
NoOfferSupplierRecord,
@@ -613,23 +613,30 @@ export class CtService {
// ─── Критерии ──────────────────────────────────────────────────────────────
async saveCriteria(
criteria: SupplierCriteria[],
ctSpId?: number,
): Promise<void> {
if (criteria.length === 0) return;
// Было: POST на каждый критерий в цикле await. Стало: один $batch (у списка «Критерии»
// капризного вебхука нет, changeset безопасен).
const bodies = criteria.map((c) => ({
Title: c.supplierId,
supplierId: parseInt(c.supplierId, 10),
deliveryType: c.deliveryType,
paymentType: c.paymentType,
deadline: c.deadline,
sertificates: c.certificates,
comment: c.comment ?? "",
...(ctSpId ? { CTId: ctSpId } : {}),
}));
// «Дополнительная информация» шага 3 → «Критерии» этой СТ целиком: прежние записи СТ (в т.ч. от ИИ)
// в корзину, текущие — заново. Раньше ручные правки и свои строки в SP не попадали вовсе.
async replaceCriteria(ctId: string, records: CriteriaRecord[]): Promise<void> {
const ctSpId = parseInt(ctId, 10);
const existing = await this.loadCriteriaForCt(ctId);
await this._runChunked(existing.map((r) => r.id as number), 20, async (id) => {
await this.http
.post(`${this._liById(LISTS.CRITERIA, id)}/recycle()`, SPHttpClient.configurations.v1, {})
.catch(() => undefined /* уже в корзине / не найден — игнорируем */);
});
// У списка «Критерии» капризного вебхука нет — пакетное создание безопасно.
const bodies = records.map((r) =>
"criteriaLabel" in r
? { Title: `${ctSpId}-${r.supplierSpId}-${r.criteriaLabel}`, CTId: ctSpId, supplierId: r.supplierSpId, criteriaLabel: r.criteriaLabel, comment: r.value }
: {
Title: String(r.supplierSpId),
CTId: ctSpId,
supplierId: r.supplierSpId,
paymentType: r.paymentType,
deliveryType: r.deliveryType,
deadline: r.deadline,
sertificates: r.sertificates,
},
);
await this.batchCreateItems(LISTS.CRITERIA, bodies);
}
@@ -661,30 +668,27 @@ export class CtService {
if (!res.ok) throw new Error(`Ошибка сохранения критериев: ${res.status}`);
}
async loadCriteria(supplierSpIds: number[]): Promise<
Array<{
supplierSpId: number;
paymentType: string;
deliveryType: string;
deadline: string;
sertificates: string;
}>
> {
if (supplierSpIds.length === 0) return [];
const filter = supplierSpIds
.map((id) => `supplierId eq ${id}`)
.join(" or ");
const url = `${this._li(LISTS.CRITERIA)}?$select=supplierId,paymentType,deliveryType,deadline,sertificates&$filter=${filter}`;
// «Критерии» этой СТ. Раньше читались по поставщику без фильтра по СТ — подтягивались условия
// того же поставщика из других СТ.
async loadCriteriaForCt(ctId: string): Promise<CriteriaRecord[]> {
const url =
`${this._li(LISTS.CRITERIA)}?$select=Id,supplierId,paymentType,deliveryType,deadline,sertificates,criteriaLabel,comment` +
`&$filter=CTId eq ${parseInt(ctId, 10)}&$top=2000`;
const res = await this.http.get(url, SPHttpClient.configurations.v1);
if (!res.ok) return [];
if (!res.ok) throw new Error(`Ошибка чтения критериев: ${res.status}`);
const data = await res.json();
return (data.value ?? []).map((item: Record<string, unknown>) => ({
supplierSpId: Number(item.supplierId),
paymentType: String(item.paymentType ?? ""),
deliveryType: String(item.deliveryType ?? ""),
deadline: String(item.deadline ?? ""),
sertificates: String(item.sertificates ?? ""),
}));
return ((data.value ?? []) as Array<Record<string, unknown>>).map((item): CriteriaRecord => {
const base = { id: Number(item.Id), supplierSpId: Number(item.supplierId) };
const label = String(item.criteriaLabel ?? "").trim();
if (label) return { ...base, criteriaLabel: label, value: String(item.comment ?? "") };
return {
...base,
paymentType: String(item.paymentType ?? ""),
deliveryType: String(item.deliveryType ?? ""),
deadline: String(item.deadline ?? ""),
sertificates: String(item.sertificates ?? ""),
};
});
}
// ─── Согласование СТ (через shared/approval — approval-route API + список