feat(ct): колонки одного поставщика с несколькими КП различаются в шапке, Adata и ИИ
У поставщика с несколькими КП колонки выглядели одинаково: в шапке подписывается файл КП, в «Дополнительной информации», ИИ-комментарии и выборе рекомендованного используется имя «Название (файл)» — ИИ сопоставляет поставщиков по имени и путал одинаковые. Adata проверяет компанию: кэш и результат проверки получают все её колонки, «Проверить всех» делает один платный запрос на компанию, а не на колонку. Этап 3 из 4 (п.2).
This commit is contained in:
+5
@@ -78,6 +78,11 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat
|
|||||||
<span className={styles.supplierName}>{s.name}</span>
|
<span className={styles.supplierName}>{s.name}</span>
|
||||||
{isRec && <span className={styles.recommendedBadge}>Рекомендуется</span>}
|
{isRec && <span className={styles.recommendedBadge}>Рекомендуется</span>}
|
||||||
</div>
|
</div>
|
||||||
|
{matrix.columnLabels[s.id] && (
|
||||||
|
<div className={styles.supplierFileLabel} title={matrix.columnLabels[s.id]}>
|
||||||
|
{matrix.columnLabels[s.id]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={styles.supplierMetaRow}>
|
<div className={styles.supplierMetaRow}>
|
||||||
<AdataIndicator supplier={s} state={adata.adataMap[s.id]} onCheck={() => void adata.checkSupplier(s.id)} />
|
<AdataIndicator supplier={s} state={adata.adataMap[s.id]} onCheck={() => void adata.checkSupplier(s.id)} />
|
||||||
{currency !== 'KZT' && <span className={styles.currencyTag}>{currency}</span>}
|
{currency !== 'KZT' && <span className={styles.currencyTag}>{currency}</span>}
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ interface Props {
|
|||||||
// Перенесено 1:1 из Step3ComparisonTable (§стайл-гайд: чистая вёрстка, логика — в useStep3Matrix).
|
// Перенесено 1:1 из Step3ComparisonTable (§стайл-гайд: чистая вёрстка, логика — в useStep3Matrix).
|
||||||
export function CriteriaTable({ matrix }: Props): JSX.Element | null {
|
export function CriteriaTable({ matrix }: Props): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
isReadOnly, suppliersWithOffer, criteriaMap, customCriteria, newCriteriaLabel,
|
isReadOnly, suppliersWithOffer, columnDisplayNames, criteriaMap, customCriteria, newCriteriaLabel,
|
||||||
setNewCriteriaLabel, updateCriteriaValue, updateCustomCriteriaValue,
|
setNewCriteriaLabel, updateCriteriaValue, updateCustomCriteriaValue,
|
||||||
handleAddCustomCriteria, handleRemoveCustomCriteria,
|
handleAddCustomCriteria, handleRemoveCustomCriteria,
|
||||||
} = matrix;
|
} = matrix;
|
||||||
@@ -38,7 +38,7 @@ export function CriteriaTable({ matrix }: Props): JSX.Element | null {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className={styles.criteriaLabelHeader} />
|
<th className={styles.criteriaLabelHeader} />
|
||||||
{suppliersWithOffer.map(s => (
|
{suppliersWithOffer.map(s => (
|
||||||
<th key={s.id} className={styles.criteriaSupplierHeader}>{s.name}</th>
|
<th key={s.id} className={styles.criteriaSupplierHeader}>{columnDisplayNames[s.id] ?? s.name}</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
+11
@@ -170,6 +170,17 @@
|
|||||||
color: $text;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Файл КП под названием — только у поставщика с несколькими КП (несколько колонок). Длинное имя
|
||||||
|
// файла обрезаем многоточием, полное — в title.
|
||||||
|
.supplierFileLabel {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
.recommendedBadge {
|
.recommendedBadge {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
+4
-3
@@ -46,7 +46,7 @@ export const Step3ComparisonTable: React.FC<Props> = ({
|
|||||||
const {
|
const {
|
||||||
isReadOnly, newSupplierName, newSupplierCurrency, setNewSupplierName, setNewSupplierCurrency,
|
isReadOnly, newSupplierName, newSupplierCurrency, setNewSupplierName, setNewSupplierCurrency,
|
||||||
handleAddSupplier, handleAddSupplierFromDatabase, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, handleSave,
|
handleAddSupplier, handleAddSupplierFromDatabase, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, handleSave,
|
||||||
extraPositions, suppliersWithOffer, allPositions, computedOffers, selectedSupplierId,
|
extraPositions, suppliersWithOffer, columnDisplayNames, allPositions, computedOffers, selectedSupplierId,
|
||||||
} = matrix;
|
} = matrix;
|
||||||
|
|
||||||
// Проверка контрагентов в Adata вынесена в хук.
|
// Проверка контрагентов в Adata вынесена в хук.
|
||||||
@@ -75,14 +75,15 @@ export const Step3ComparisonTable: React.FC<Props> = ({
|
|||||||
],
|
],
|
||||||
suppliers: suppliersWithOffer
|
suppliers: suppliersWithOffer
|
||||||
.map(s => ({
|
.map(s => ({
|
||||||
name: s.name,
|
// Различимое имя: у поставщика с несколькими КП иначе в тексте будут два одинаковых.
|
||||||
|
name: columnDisplayNames[s.id] ?? s.name,
|
||||||
grandTotal: allPositions.reduce((sum, pos) => {
|
grandTotal: allPositions.reduce((sum, pos) => {
|
||||||
const posOffers = computedOffers.filter(o => o.supplierId === s.id && o.positionId === pos.id);
|
const posOffers = computedOffers.filter(o => o.supplierId === s.id && o.positionId === pos.id);
|
||||||
return sum + posOffers.reduce((s2, o) => s2 + (o.totalWithExtraKzt ?? 0), 0);
|
return sum + posOffers.reduce((s2, o) => s2 + (o.totalWithExtraKzt ?? 0), 0);
|
||||||
}, 0),
|
}, 0),
|
||||||
}))
|
}))
|
||||||
.filter(s => s.grandTotal > 0),
|
.filter(s => s.grandTotal > 0),
|
||||||
recommendedSupplier: suppliersWithOffer.find(s => s.id === selectedSupplierId)?.name,
|
recommendedSupplier: selectedSupplierId ? (columnDisplayNames[selectedSupplierId] ?? undefined) : undefined,
|
||||||
}),
|
}),
|
||||||
onPdfError: setSaveError,
|
onPdfError: setSaveError,
|
||||||
});
|
});
|
||||||
|
|||||||
+28
-6
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { CtService } from '../../services/CtService';
|
import { CtService } from '../../services/CtService';
|
||||||
import { AdataCompanyInfo, Supplier } from '../types/domain';
|
import { AdataCompanyInfo, Supplier } from '../types/domain';
|
||||||
|
import { findSiblingColumnIds } from '../utils/supplierColumns';
|
||||||
|
|
||||||
export interface AdataSupplierState {
|
export interface AdataSupplierState {
|
||||||
bin?: string;
|
bin?: string;
|
||||||
@@ -38,9 +39,11 @@ export function useAdataCheck(service: CtService, suppliers: Supplier[]): UseAda
|
|||||||
.then(rows => {
|
.then(rows => {
|
||||||
setAdataMap(prev => {
|
setAdataMap(prev => {
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
|
// Кэш — на компанию: у поставщика с несколькими КП он нужен каждой его колонке.
|
||||||
rows.forEach(r => {
|
rows.forEach(r => {
|
||||||
const sup = suppliersRef.current.find(s => s.spId === r.supplierSpId);
|
suppliersRef.current
|
||||||
if (!sup) return;
|
.filter(s => s.spId === r.supplierSpId)
|
||||||
|
.forEach(sup => {
|
||||||
next[sup.id] = {
|
next[sup.id] = {
|
||||||
...next[sup.id],
|
...next[sup.id],
|
||||||
bin: r.bin,
|
bin: r.bin,
|
||||||
@@ -49,6 +52,7 @@ export function useAdataCheck(service: CtService, suppliers: Supplier[]): UseAda
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -63,22 +67,40 @@ export function useAdataCheck(service: CtService, suppliers: Supplier[]): UseAda
|
|||||||
setAdataMap(prev => ({ ...prev, [supplierId]: { ...prev[supplierId], isLoading: false, error: 'БИН поставщика неизвестен — проверка недоступна' } }));
|
setAdataMap(prev => ({ ...prev, [supplierId]: { ...prev[supplierId], isLoading: false, error: 'БИН поставщика неизвестен — проверка недоступна' } }));
|
||||||
return;
|
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 {
|
try {
|
||||||
const data = await service.fetchAdataInfo(bin);
|
const data = await service.fetchAdataInfo(bin);
|
||||||
const checkedAt = new Date().toISOString();
|
const checkedAt = new Date().toISOString();
|
||||||
setAdataMap(prev => ({ ...prev, [supplierId]: { bin, data, checkedAt, isLoading: false } }));
|
patchColumns(() => ({ bin, data, checkedAt, isLoading: false }));
|
||||||
if (sup.spId !== undefined) {
|
if (sup.spId !== undefined) {
|
||||||
service.saveAdataResult(sup.spId, data).catch(() => undefined);
|
service.saveAdataResult(sup.spId, data).catch(() => undefined);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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]);
|
}, [service]);
|
||||||
|
|
||||||
const checkAllSuppliers = useCallback(async (): Promise<void> => {
|
const checkAllSuppliers = useCallback(async (): Promise<void> => {
|
||||||
setIsCheckingAll(true);
|
setIsCheckingAll(true);
|
||||||
const targets = suppliersRef.current.filter(s => s.hasOffer && (adataMapRef.current[s.id]?.bin ?? s.bin));
|
// Одна проверка на компанию: колонки того же поставщика получат результат через checkSupplier.
|
||||||
|
const checkedSpIds = new Set<number>();
|
||||||
|
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)));
|
await Promise.all(targets.map(s => checkSupplier(s.id)));
|
||||||
setIsCheckingAll(false);
|
setIsCheckingAll(false);
|
||||||
}, [checkSupplier]);
|
}, [checkSupplier]);
|
||||||
|
|||||||
+4
-3
@@ -30,7 +30,7 @@ export function useRecommendedPick(service: CtService, matrix: Step3Matrix): Use
|
|||||||
const [pickError, setPickError] = useState<string | undefined>(undefined);
|
const [pickError, setPickError] = useState<string | undefined>(undefined);
|
||||||
const [recommendedId, setRecommendedId] = useState<string | undefined>(undefined);
|
const [recommendedId, setRecommendedId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const { suppliersWithOffer, criteriaMap, computedOffers, allPositions, setSelectedSupplierId } = matrix;
|
const { suppliersWithOffer, columnDisplayNames, criteriaMap, computedOffers, allPositions, setSelectedSupplierId } = matrix;
|
||||||
|
|
||||||
const allPaymentTermsFilled = suppliersWithOffer.length > 0
|
const allPaymentTermsFilled = suppliersWithOffer.length > 0
|
||||||
&& suppliersWithOffer.every(s => (criteriaMap[s.id]?.paymentType ?? '').trim().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
|
const totals = suppliersWithOffer
|
||||||
.map(s => ({
|
.map(s => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
name: s.name,
|
// Различимое имя: ответ ИИ сопоставляется по имени, у поставщика с несколькими КП оно повторяется.
|
||||||
|
name: columnDisplayNames[s.id] ?? s.name,
|
||||||
paymentType: criteriaMap[s.id]?.paymentType ?? '',
|
paymentType: criteriaMap[s.id]?.paymentType ?? '',
|
||||||
total: allPositions.reduce((sum, pos) => {
|
total: allPositions.reduce((sum, pos) => {
|
||||||
const posOffers = computedOffers.filter(o => o.supplierId === s.id && o.positionId === pos.id);
|
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 {
|
} finally {
|
||||||
setIsPicking(false);
|
setIsPicking(false);
|
||||||
}
|
}
|
||||||
}, [allPaymentTermsFilled, suppliersWithOffer, criteriaMap, computedOffers, allPositions, service, setSelectedSupplierId]);
|
}, [allPaymentTermsFilled, suppliersWithOffer, columnDisplayNames, criteriaMap, computedOffers, allPositions, service, setSelectedSupplierId]);
|
||||||
|
|
||||||
return { isPicking, pickError, allPaymentTermsFilled, recommendedId, pickRecommendedSupplier };
|
return { isPicking, pickError, allPaymentTermsFilled, recommendedId, pickRecommendedSupplier };
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -6,7 +6,7 @@ import { CTFormData } from '../hooks/useCtForm';
|
|||||||
import { ExtraSupplier, ExtraPosition, PositionRow, CellEdit, EditableOffers, CriteriaEntry } from './matrixTypes';
|
import { ExtraSupplier, ExtraPosition, PositionRow, CellEdit, EditableOffers, CriteriaEntry } from './matrixTypes';
|
||||||
import { applyCreatedIds, buildOfferKey, computeOffer } from './matrixHelpers';
|
import { applyCreatedIds, buildOfferKey, computeOffer } from './matrixHelpers';
|
||||||
import { IVendorRecord } from '../../services/VendorDatabaseService';
|
import { IVendorRecord } from '../../services/VendorDatabaseService';
|
||||||
import { findColumnForRecord } from '../utils/supplierColumns';
|
import { buildColumnDisplayNames, buildColumnLabels, findColumnForRecord } from '../utils/supplierColumns';
|
||||||
|
|
||||||
let extraIdCounter = 0;
|
let extraIdCounter = 0;
|
||||||
|
|
||||||
@@ -310,6 +310,17 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
|
|||||||
})),
|
})),
|
||||||
], [suppliers, extraSuppliers]);
|
], [suppliers, extraSuppliers]);
|
||||||
|
|
||||||
|
// У поставщика может быть несколько КП (колонок) — подписываем их файлом КП, а в текстах и ИИ
|
||||||
|
// используем различимые имена «Название (файл)».
|
||||||
|
const columnLabels = useMemo(
|
||||||
|
() => buildColumnLabels(suppliersWithOffer, formData.uploadedKpFiles ?? []),
|
||||||
|
[suppliersWithOffer, formData.uploadedKpFiles],
|
||||||
|
);
|
||||||
|
const columnDisplayNames = useMemo(
|
||||||
|
() => buildColumnDisplayNames(suppliersWithOffer, columnLabels),
|
||||||
|
[suppliersWithOffer, columnLabels],
|
||||||
|
);
|
||||||
|
|
||||||
const allPositions = useMemo((): PositionRow[] => {
|
const allPositions = useMemo((): PositionRow[] => {
|
||||||
// Extra positions take priority: if a selected position shares a name with an extra, hide it from the regular section
|
// 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()));
|
const extraNames = new Set(extraPositions.map(ep => ep.materialName.toLowerCase().trim()));
|
||||||
@@ -888,6 +899,8 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
|
|||||||
setSaveError,
|
setSaveError,
|
||||||
// derived
|
// derived
|
||||||
suppliersWithOffer,
|
suppliersWithOffer,
|
||||||
|
columnLabels,
|
||||||
|
columnDisplayNames,
|
||||||
allPositions,
|
allPositions,
|
||||||
regularPositions,
|
regularPositions,
|
||||||
extraPositionsRows,
|
extraPositionsRows,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './supplierColumns';
|
import { buildColumnDisplayNames, buildColumnLabels, buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord, findSiblingColumnIds } from './supplierColumns';
|
||||||
import { Supplier } from '../types/domain';
|
import { Supplier } from '../types/domain';
|
||||||
|
|
||||||
describe('buildSupplierColumnId', () => {
|
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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -43,3 +43,46 @@ export function buildColumnsFromSpOffers(records: SpOfferColumnSource[]): Suppli
|
|||||||
});
|
});
|
||||||
return Array.from(columnsById.values());
|
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<string, string> {
|
||||||
|
const labels: Record<string, string> = {};
|
||||||
|
const columnsBySupplier = new Map<number, Supplier[]>();
|
||||||
|
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<string, string>,
|
||||||
|
): Record<string, string> {
|
||||||
|
const names: Record<string, string> = {};
|
||||||
|
columns.forEach(c => {
|
||||||
|
names[c.id] = labels[c.id] ? `${c.name} (${labels[c.id]})` : c.name;
|
||||||
|
});
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user