diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx
index 5385052..930a10a 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx
+++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx
@@ -78,6 +78,11 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat
{s.name}
{isRec && Рекомендуется}
+ {matrix.columnLabels[s.id] && (
+
+ {matrix.columnLabels[s.id]}
+
+ )}
void adata.checkSupplier(s.id)} />
{currency !== 'KZT' && {currency}}
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx
index 92985cb..274d900 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx
+++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx
@@ -13,7 +13,7 @@ interface Props {
// Перенесено 1:1 из Step3ComparisonTable (§стайл-гайд: чистая вёрстка, логика — в useStep3Matrix).
export function CriteriaTable({ matrix }: Props): JSX.Element | null {
const {
- isReadOnly, suppliersWithOffer, criteriaMap, customCriteria, newCriteriaLabel,
+ isReadOnly, suppliersWithOffer, columnDisplayNames, criteriaMap, customCriteria, newCriteriaLabel,
setNewCriteriaLabel, updateCriteriaValue, updateCustomCriteriaValue,
handleAddCustomCriteria, handleRemoveCustomCriteria,
} = matrix;
@@ -38,7 +38,7 @@ export function CriteriaTable({ matrix }: Props): JSX.Element | null {
|
{suppliersWithOffer.map(s => (
- {s.name} |
+ {columnDisplayNames[s.id] ?? s.name} |
))}
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss
index 5922478..162b95e 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss
+++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss
@@ -170,6 +170,17 @@
color: $text;
}
+// Файл КП под названием — только у поставщика с несколькими КП (несколько колонок). Длинное имя
+// файла обрезаем многоточием, полное — в title.
+.supplierFileLabel {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 11px;
+ color: $muted;
+}
+
.recommendedBadge {
font-size: 10px;
font-weight: 600;
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx
index 8f70504..3509ec1 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx
+++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx
@@ -46,7 +46,7 @@ export const Step3ComparisonTable: React.FC = ({
const {
isReadOnly, newSupplierName, newSupplierCurrency, setNewSupplierName, setNewSupplierCurrency,
handleAddSupplier, handleAddSupplierFromDatabase, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, handleSave,
- extraPositions, suppliersWithOffer, allPositions, computedOffers, selectedSupplierId,
+ extraPositions, suppliersWithOffer, columnDisplayNames, allPositions, computedOffers, selectedSupplierId,
} = matrix;
// Проверка контрагентов в Adata вынесена в хук.
@@ -75,14 +75,15 @@ export const Step3ComparisonTable: React.FC = ({
],
suppliers: suppliersWithOffer
.map(s => ({
- name: s.name,
+ // Различимое имя: у поставщика с несколькими КП иначе в тексте будут два одинаковых.
+ name: columnDisplayNames[s.id] ?? s.name,
grandTotal: allPositions.reduce((sum, pos) => {
const posOffers = computedOffers.filter(o => o.supplierId === s.id && o.positionId === pos.id);
return sum + posOffers.reduce((s2, o) => s2 + (o.totalWithExtraKzt ?? 0), 0);
}, 0),
}))
.filter(s => s.grandTotal > 0),
- recommendedSupplier: suppliersWithOffer.find(s => s.id === selectedSupplierId)?.name,
+ recommendedSupplier: selectedSupplierId ? (columnDisplayNames[selectedSupplierId] ?? undefined) : undefined,
}),
onPdfError: setSaveError,
});
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useAdataCheck.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useAdataCheck.ts
index e1fc854..07c387d 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useAdataCheck.ts
+++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useAdataCheck.ts
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { CtService } from '../../services/CtService';
import { AdataCompanyInfo, Supplier } from '../types/domain';
+import { findSiblingColumnIds } from '../utils/supplierColumns';
export interface AdataSupplierState {
bin?: string;
@@ -38,16 +39,19 @@ export function useAdataCheck(service: CtService, suppliers: Supplier[]): UseAda
.then(rows => {
setAdataMap(prev => {
const next = { ...prev };
+ // Кэш — на компанию: у поставщика с несколькими КП он нужен каждой его колонке.
rows.forEach(r => {
- const sup = suppliersRef.current.find(s => s.spId === r.supplierSpId);
- if (!sup) return;
- next[sup.id] = {
- ...next[sup.id],
- bin: r.bin,
- data: r.adataData,
- checkedAt: r.adataCheckedAt,
- isLoading: false,
- };
+ suppliersRef.current
+ .filter(s => s.spId === r.supplierSpId)
+ .forEach(sup => {
+ next[sup.id] = {
+ ...next[sup.id],
+ bin: r.bin,
+ data: r.adataData,
+ checkedAt: r.adataCheckedAt,
+ isLoading: false,
+ };
+ });
});
return next;
});
@@ -63,22 +67,40 @@ export function useAdataCheck(service: CtService, suppliers: Supplier[]): UseAda
setAdataMap(prev => ({ ...prev, [supplierId]: { ...prev[supplierId], isLoading: false, error: 'БИН поставщика неизвестен — проверка недоступна' } }));
return;
}
- setAdataMap(prev => ({ ...prev, [supplierId]: { ...prev[supplierId], bin, isLoading: true, error: undefined } }));
+ // Adata проверяет компанию — результат раздаём всем колонкам этого поставщика (несколько КП).
+ const columnIds = findSiblingColumnIds(suppliersRef.current, supplierId);
+ const patchColumns = (state: (prevState: AdataSupplierState | undefined) => AdataSupplierState): void => {
+ setAdataMap(prev => {
+ const next = { ...prev };
+ columnIds.forEach(id => { next[id] = state(prev[id]); });
+ return next;
+ });
+ };
+ patchColumns(prevState => ({ ...prevState, bin, isLoading: true, error: undefined }));
try {
const data = await service.fetchAdataInfo(bin);
const checkedAt = new Date().toISOString();
- setAdataMap(prev => ({ ...prev, [supplierId]: { bin, data, checkedAt, isLoading: false } }));
+ patchColumns(() => ({ bin, data, checkedAt, isLoading: false }));
if (sup.spId !== undefined) {
service.saveAdataResult(sup.spId, data).catch(() => undefined);
}
} catch (err) {
- setAdataMap(prev => ({ ...prev, [supplierId]: { ...prev[supplierId], bin, isLoading: false, error: (err as Error).message } }));
+ patchColumns(prevState => ({ ...prevState, bin, isLoading: false, error: (err as Error).message }));
}
}, [service]);
const checkAllSuppliers = useCallback(async (): Promise => {
setIsCheckingAll(true);
- const targets = suppliersRef.current.filter(s => s.hasOffer && (adataMapRef.current[s.id]?.bin ?? s.bin));
+ // Одна проверка на компанию: колонки того же поставщика получат результат через checkSupplier.
+ const checkedSpIds = new Set();
+ const targets = suppliersRef.current
+ .filter(s => s.hasOffer && (adataMapRef.current[s.id]?.bin ?? s.bin))
+ .filter(s => {
+ if (s.spId === undefined) return true;
+ if (checkedSpIds.has(s.spId)) return false;
+ checkedSpIds.add(s.spId);
+ return true;
+ });
await Promise.all(targets.map(s => checkSupplier(s.id)));
setIsCheckingAll(false);
}, [checkSupplier]);
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useRecommendedPick.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useRecommendedPick.ts
index 99f03a4..fa1f61b 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useRecommendedPick.ts
+++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useRecommendedPick.ts
@@ -30,7 +30,7 @@ export function useRecommendedPick(service: CtService, matrix: Step3Matrix): Use
const [pickError, setPickError] = useState(undefined);
const [recommendedId, setRecommendedId] = useState(undefined);
- const { suppliersWithOffer, criteriaMap, computedOffers, allPositions, setSelectedSupplierId } = matrix;
+ const { suppliersWithOffer, columnDisplayNames, criteriaMap, computedOffers, allPositions, setSelectedSupplierId } = matrix;
const allPaymentTermsFilled = suppliersWithOffer.length > 0
&& suppliersWithOffer.every(s => (criteriaMap[s.id]?.paymentType ?? '').trim().length > 0);
@@ -43,7 +43,8 @@ export function useRecommendedPick(service: CtService, matrix: Step3Matrix): Use
const totals = suppliersWithOffer
.map(s => ({
id: s.id,
- name: s.name,
+ // Различимое имя: ответ ИИ сопоставляется по имени, у поставщика с несколькими КП оно повторяется.
+ name: columnDisplayNames[s.id] ?? s.name,
paymentType: criteriaMap[s.id]?.paymentType ?? '',
total: allPositions.reduce((sum, pos) => {
const posOffers = computedOffers.filter(o => o.supplierId === s.id && o.positionId === pos.id);
@@ -81,7 +82,7 @@ export function useRecommendedPick(service: CtService, matrix: Step3Matrix): Use
} finally {
setIsPicking(false);
}
- }, [allPaymentTermsFilled, suppliersWithOffer, criteriaMap, computedOffers, allPositions, service, setSelectedSupplierId]);
+ }, [allPaymentTermsFilled, suppliersWithOffer, columnDisplayNames, criteriaMap, computedOffers, allPositions, service, setSelectedSupplierId]);
return { isPicking, pickError, allPaymentTermsFilled, recommendedId, pickRecommendedSupplier };
}
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts
index 432099c..0edce42 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts
+++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useStep3Matrix.ts
@@ -6,7 +6,7 @@ import { CTFormData } from '../hooks/useCtForm';
import { ExtraSupplier, ExtraPosition, PositionRow, CellEdit, EditableOffers, CriteriaEntry } from './matrixTypes';
import { applyCreatedIds, buildOfferKey, computeOffer } from './matrixHelpers';
import { IVendorRecord } from '../../services/VendorDatabaseService';
-import { findColumnForRecord } from '../utils/supplierColumns';
+import { buildColumnDisplayNames, buildColumnLabels, findColumnForRecord } from '../utils/supplierColumns';
let extraIdCounter = 0;
@@ -310,6 +310,17 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
})),
], [suppliers, extraSuppliers]);
+ // У поставщика может быть несколько КП (колонок) — подписываем их файлом КП, а в текстах и ИИ
+ // используем различимые имена «Название (файл)».
+ const columnLabels = useMemo(
+ () => buildColumnLabels(suppliersWithOffer, formData.uploadedKpFiles ?? []),
+ [suppliersWithOffer, formData.uploadedKpFiles],
+ );
+ const columnDisplayNames = useMemo(
+ () => buildColumnDisplayNames(suppliersWithOffer, columnLabels),
+ [suppliersWithOffer, columnLabels],
+ );
+
const allPositions = useMemo((): PositionRow[] => {
// Extra positions take priority: if a selected position shares a name with an extra, hide it from the regular section
const extraNames = new Set(extraPositions.map(ep => ep.materialName.toLowerCase().trim()));
@@ -888,6 +899,8 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
setSaveError,
// derived
suppliersWithOffer,
+ columnLabels,
+ columnDisplayNames,
allPositions,
regularPositions,
extraPositionsRows,
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.test.ts
index f12137c..b89def1 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.test.ts
+++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.test.ts
@@ -1,4 +1,4 @@
-import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './supplierColumns';
+import { buildColumnDisplayNames, buildColumnLabels, buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord, findSiblingColumnIds } from './supplierColumns';
import { Supplier } from '../types/domain';
describe('buildSupplierColumnId', () => {
@@ -46,3 +46,49 @@ describe('buildColumnsFromSpOffers (открытие по ссылке / без
]);
});
});
+
+describe('подписи колонок поставщика с несколькими КП', () => {
+ const ARLAN = 'ТОО «Арлан Офис Снаб»';
+ const columns: Supplier[] = [
+ { id: '263_277', spId: 263, kpFileSpId: 277, name: ARLAN, hasOffer: true },
+ { id: '264_278', spId: 264, kpFileSpId: 278, name: 'ТОО «Кала Саплай»', hasOffer: true },
+ { id: '263_285', spId: 263, kpFileSpId: 285, name: ARLAN, hasOffer: true },
+ ];
+ const uploaded = [
+ { spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf' },
+ { spId: 278, name: 'KP_02_Qala_Supply_KZT.pdf' },
+ { spId: 285, name: 'KP_01_Arlan_Office_USD.pdf' },
+ ];
+
+ // Две колонки «Арлан» в шапке неотличимы — подписываем файлом КП, но только их.
+ it('buildColumnLabels: файл КП — только у колонок поставщика с несколькими КП', () => {
+ expect(buildColumnLabels(columns, uploaded)).toEqual({
+ '263_277': 'KP_01_Arlan_Office_KZT.pdf',
+ '263_285': 'KP_01_Arlan_Office_USD.pdf',
+ });
+ });
+
+ it('buildColumnLabels: файл неизвестен — номер КП по порядку', () => {
+ expect(buildColumnLabels(columns, [])).toEqual({ '263_277': 'КП 1', '263_285': 'КП 2' });
+ });
+
+ // ИИ-комментарий и выбор рекомендованного сопоставляют поставщиков по имени — имена должны различаться.
+ it('buildColumnDisplayNames: имя с подписью для повторяющихся, просто имя для остальных', () => {
+ const names = buildColumnDisplayNames(columns, buildColumnLabels(columns, uploaded));
+
+ expect(names).toEqual({
+ '263_277': `${ARLAN} (KP_01_Arlan_Office_KZT.pdf)`,
+ '264_278': 'ТОО «Кала Саплай»',
+ '263_285': `${ARLAN} (KP_01_Arlan_Office_USD.pdf)`,
+ });
+ });
+
+ // Adata проверяет компанию: результат нужен всем её колонкам, а платный запрос — один.
+ it('findSiblingColumnIds: все колонки той же компании', () => {
+ expect(findSiblingColumnIds(columns, '263_285')).toEqual(['263_277', '263_285']);
+ });
+
+ it('findSiblingColumnIds: поставщик без SP Id — только сама колонка', () => {
+ expect(findSiblingColumnIds([{ id: 'extra_sup_1', name: 'Ручной', hasOffer: true }], 'extra_sup_1')).toEqual(['extra_sup_1']);
+ });
+});
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.ts
index d5ddfbb..37263e7 100644
--- a/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.ts
+++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/supplierColumns.ts
@@ -43,3 +43,46 @@ export function buildColumnsFromSpOffers(records: SpOfferColumnSource[]): Suppli
});
return Array.from(columnsById.values());
}
+
+// Колонки той же компании (тот же SP Id поставщика), включая саму колонку. Нужно для данных, которые
+// принадлежат компании, а не КП: проверка Adata, БИН.
+export function findSiblingColumnIds(columns: Supplier[], columnId: string): string[] {
+ const column = columns.find(c => c.id === columnId);
+ if (column?.spId === undefined) return [columnId];
+ return columns.filter(c => c.spId === column.spId).map(c => c.id);
+}
+
+// Подписи колонок поставщика с несколькими КП — имя файла КП (без файла — «КП N» по порядку).
+// У поставщика с одним КП подписи нет: в шапке хватает названия.
+export function buildColumnLabels(
+ columns: Supplier[],
+ uploadedKpFiles: Array<{ spId: number; name: string }>,
+): Record {
+ const labels: Record = {};
+ const columnsBySupplier = new Map();
+ columns.forEach(c => {
+ if (c.spId === undefined) return;
+ columnsBySupplier.set(c.spId, [...(columnsBySupplier.get(c.spId) ?? []), c]);
+ });
+ columnsBySupplier.forEach(group => {
+ if (group.length < 2) return;
+ group.forEach((c, index) => {
+ const fileName = uploadedKpFiles.find(f => f.spId === c.kpFileSpId)?.name;
+ labels[c.id] = fileName ?? `КП ${index + 1}`;
+ });
+ });
+ return labels;
+}
+
+// Различимые имена колонок для текстов и ИИ (комментарий, выбор рекомендованного), где поставщиков
+// сопоставляют по имени: «Название (файл КП)» у повторяющихся, просто название у остальных.
+export function buildColumnDisplayNames(
+ columns: Supplier[],
+ labels: Record,
+): Record {
+ const names: Record = {};
+ columns.forEach(c => {
+ names[c.id] = labels[c.id] ? `${c.name} (${labels[c.id]})` : c.name;
+ });
+ return names;
+}