feat(ct): лишний КП поставщика убирается корзиной вместо окна «заменить / добавить»
Окно на каждый второй КП поставщика при пакетной загрузке (6 файлов — 6 окон) было непонятно.
Теперь вопросов при загрузке нет: каждый КП — своя колонка. В «Загруженных КП» файлы одного
поставщика сгруппированы с пометкой «N КП — сравниваются отдельными колонками», а корзина
(и крестик у обработанного файла) убирает КП из сравнения — колонку, цены и записи этого
файла в SP, с подтверждением. Раньше корзина только прятала строку, колонка оставалась.
Так заменяют исправленный КП: загружают новый, старый убирают.
Заменяет окно из 744b603 (п.2).
This commit is contained in:
-41
@@ -1,41 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Dialog } from '@tnsu/ui-kit';
|
||||
import { KpReplaceChoice } from './kpReplace';
|
||||
import { KpReplaceQuestion } from './useKpReplaceQuestion';
|
||||
import styles from './Step2CommercialOffers.module.scss';
|
||||
|
||||
interface Props {
|
||||
question: KpReplaceQuestion;
|
||||
onAnswer: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
// Окно выбора для второго КП того же поставщика. Закрытие крестиком/Esc = «Добавить отдельно»:
|
||||
// безопасный вариант, ничего не удаляет.
|
||||
export const KpReplaceDialog: React.FC<Props> = ({ question, onAnswer }) => (
|
||||
<Dialog
|
||||
open
|
||||
title="У поставщика уже есть КП"
|
||||
subText={`${question.supplierName}: загружен ещё один КП — ${question.fileName}`}
|
||||
onDismiss={() => onAnswer('separate')}
|
||||
width="md"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" onClick={() => onAnswer('separate')}>
|
||||
Добавить отдельным предложением
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => onAnswer('replace')}>
|
||||
Заменить
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>Уже загружено:</p>
|
||||
<ul className={styles.replaceExistingList}>
|
||||
{question.existing.map(f => <li key={f.spId ?? f.name}>{f.name}</li>)}
|
||||
</ul>
|
||||
<p className={styles.replaceHint}>
|
||||
«Заменить» — прежний КП уберётся из сравнения, останется новый (исправленный КП, тот же файл повторно).
|
||||
«Добавить отдельным предложением» — оба КП будут отдельными колонками (напр. другая валюта или вариант).
|
||||
</p>
|
||||
</Dialog>
|
||||
);
|
||||
+13
-8
@@ -207,15 +207,20 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// Окно «У поставщика уже есть КП» (KpReplaceDialog).
|
||||
.replaceExistingList {
|
||||
margin: 4px 0 12px;
|
||||
padding-left: 20px;
|
||||
font-weight: 600;
|
||||
// «Загруженные КП»: файлы одного поставщика сгруппированы, у нескольких КП — пометка.
|
||||
.kpGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.replaceHint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
.kpGroupHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kpGroupNote {
|
||||
font-size: 12px;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
+46
-7
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Button, Alert, Spinner } from '@tnsu/ui-kit';
|
||||
import { Button, Alert, Spinner, confirmModal } from '@tnsu/ui-kit';
|
||||
import { CurrencyRate, FormStep, Supplier } from '../types/domain';
|
||||
import { CtService } from '../../services/CtService';
|
||||
import { CTFormData } from '../hooks/useCtForm';
|
||||
@@ -11,7 +11,7 @@ import { useKpProcessing } from './useKpProcessing';
|
||||
import { persistStep2 } from './step2Persist';
|
||||
import { SuppliersWithoutKp } from './SuppliersWithoutKp';
|
||||
import { UploadedKpList } from './UploadedKpList';
|
||||
import { KpReplaceDialog } from './KpReplaceDialog';
|
||||
import { removeKpFileFromComparison } from './kpRemoval';
|
||||
import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency';
|
||||
import styles from './Step2CommercialOffers.module.scss';
|
||||
|
||||
@@ -92,6 +92,48 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
||||
.forEach(f => kp.handleCurrencyChange(f.localId, currencyCode));
|
||||
};
|
||||
|
||||
// Убрать КП из сравнения (корзина в «Загруженные КП» или крестик у обработанного файла): так
|
||||
// заменяют исправленный КП — загружают новый, старый убирают. Колонка и цены уходят сразу,
|
||||
// записи этого файла в SP — в корзину. Сам файл остаётся в библиотеке.
|
||||
const handleRemoveKpFile = async (fileSpId: number): Promise<void> => {
|
||||
const file = formData.uploadedKpFiles.find(u => u.spId === fileSpId);
|
||||
if (!file) {
|
||||
kp.removeFileBySpId(fileSpId);
|
||||
return;
|
||||
}
|
||||
const confirmed = await confirmModal({
|
||||
title: 'Убрать КП из сравнения?',
|
||||
message: `${file.name}${file.supplierName ? ` (${file.supplierName})` : ''} — его колонка и цены уйдут из таблицы сравнения. Файл останется в библиотеке КП.`,
|
||||
confirmText: 'Убрать',
|
||||
danger: true,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
kp.removeFileBySpId(fileSpId);
|
||||
onSave(removeKpFileFromComparison(
|
||||
{ suppliers: formData.suppliers ?? [], offers: formData.offers ?? [], uploadedKpFiles: formData.uploadedKpFiles },
|
||||
file,
|
||||
));
|
||||
const positionSpIds = [...formData.selectedPositions, ...(formData.step3ExtraPositions ?? [])]
|
||||
.map(p => p.spId)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
service.recycleKpFileOffers(fileSpId, positionSpIds).catch(err => {
|
||||
// Не критично: «Сохранить» на Шаге 2/3 перезапишет предложения позиций по текущей таблице.
|
||||
console.warn('[КП] не удалось убрать предложения КП из SP:', err);
|
||||
});
|
||||
};
|
||||
|
||||
// Крестик в очереди загрузки: обработанный КП уже в сравнении — убираем его так же, как корзиной;
|
||||
// файл в процессе или с ошибкой — просто убираем из очереди.
|
||||
const handleQueueFileRemove = (localId: string): void => {
|
||||
const file = kp.kpFiles.find(f => f.localId === localId);
|
||||
if (file?.status === 'done' && file.spId !== undefined) {
|
||||
handleRemoveKpFile(file.spId).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
kp.handleFileRemove(localId);
|
||||
};
|
||||
|
||||
const handleSave = async (): Promise<void> => {
|
||||
setIsSaving(true);
|
||||
setSaveError(undefined);
|
||||
@@ -130,15 +172,12 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
||||
<FileUploadArea
|
||||
files={uploadedFiles}
|
||||
onFilesAdded={files => { kp.handleFilesAdded(files).catch(() => undefined); }}
|
||||
onFileRemove={kp.handleFileRemove}
|
||||
onFileRemove={handleQueueFileRemove}
|
||||
currencyOptions={currencyOptions}
|
||||
onCurrencyChange={kp.handleCurrencyChange}
|
||||
disabled={isNotificationLink || isReadOnly}
|
||||
/>
|
||||
{kp.duplicateWarning && <Alert variant="warn">{kp.duplicateWarning}</Alert>}
|
||||
{kp.replaceQuestion && (
|
||||
<KpReplaceDialog question={kp.replaceQuestion} onAnswer={kp.answerReplaceQuestion} />
|
||||
)}
|
||||
{kp.isProcessing && (
|
||||
<Alert variant="info">Файлы обрабатываются системой Syntex. Можно продолжить заполнение формы.</Alert>
|
||||
)}
|
||||
@@ -159,7 +198,7 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
||||
currencyOptions={currencyOptions}
|
||||
getCurrency={file => getSupplierCurrency(formData.suppliers ?? [], file)}
|
||||
onCurrencyChange={handleUploadedCurrencyChange}
|
||||
onRemove={(spId) => onSave({ uploadedKpFiles: formData.uploadedKpFiles.filter(u => u.spId !== spId) })}
|
||||
onRemove={spId => { handleRemoveKpFile(spId).catch(() => undefined); }}
|
||||
disabled={isNotificationLink || isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
+24
-44
@@ -1,8 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { IconButton } from '@tnsu/ui-kit';
|
||||
import { CtService } from '../../services/CtService';
|
||||
import { CTFormData } from '../hooks/useCtForm';
|
||||
import { KpFileRef } from './kpCurrency';
|
||||
import { groupUploadedKpFiles } from './kpRemoval';
|
||||
import { UploadedKpRow } from './UploadedKpRow';
|
||||
import styles from './Step2CommercialOffers.module.scss';
|
||||
|
||||
interface Props {
|
||||
@@ -15,54 +16,33 @@ interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// Секция «Загруженные КП» (постоянный список из formData): ссылка на файл, валюта КП и удаление.
|
||||
// Валюту здесь можно поправить и после возврата с Шага 3 — очередь загрузки к этому моменту уже пуста.
|
||||
export const UploadedKpList: React.FC<Props> = ({
|
||||
files,
|
||||
service,
|
||||
currencyOptions,
|
||||
getCurrency,
|
||||
onCurrencyChange,
|
||||
onRemove,
|
||||
disabled,
|
||||
}) => (
|
||||
// Секция «Загруженные КП» (постоянный список из formData), сгруппированная по поставщику. Каждый КП —
|
||||
// отдельная колонка сравнения; несколько КП одного поставщика стоят рядом с пометкой, лишний
|
||||
// (напр. старая версия КП) убирают корзиной. Валюту можно поправить и после возврата с Шага 3.
|
||||
export const UploadedKpList: React.FC<Props> = ({ files, ...rowProps }) => (
|
||||
<section className={styles.section}>
|
||||
<h3 className={styles.sectionTitle}>Загруженные КП</h3>
|
||||
<ul className={styles.noOfferList}>
|
||||
{files.map(f => (
|
||||
<li key={f.spId} className={styles.noOfferItem}>
|
||||
{f.supplierName && <span className={styles.noOfferName}>{f.supplierName}</span>}
|
||||
<a
|
||||
href={service.getKpFileUrl(f.name)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.kpFileLink}
|
||||
>
|
||||
{f.name}
|
||||
</a>
|
||||
{disabled ? (
|
||||
<span className={styles.kpCurrencyCode}>{getCurrency(f)}</span>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
className={styles.kpCurrencySelect}
|
||||
value={getCurrency(f)}
|
||||
onChange={e => onCurrencyChange(f.spId, e.target.value)}
|
||||
// Валюта хранится на поставщике — без него менять нечего.
|
||||
disabled={!f.supplierName}
|
||||
aria-label={`Валюта КП ${f.name}`}
|
||||
>
|
||||
{currencyOptions.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
<IconButton
|
||||
icon="trash"
|
||||
title="Удалить КП из списка"
|
||||
onClick={() => onRemove(f.spId)}
|
||||
/>
|
||||
</>
|
||||
{groupUploadedKpFiles(files).map(group => {
|
||||
const isMultiple = group.files.length > 1;
|
||||
return (
|
||||
<li key={group.files[0].spId} className={styles.kpGroup}>
|
||||
{isMultiple && (
|
||||
<div className={styles.kpGroupHeader}>
|
||||
<span className={styles.noOfferName}>{group.supplierName}</span>
|
||||
<span className={styles.kpGroupNote}>
|
||||
{group.files.length} КП — сравниваются отдельными колонками; лишний уберите корзиной
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
<ul className={styles.noOfferList}>
|
||||
{group.files.map(file => (
|
||||
<UploadedKpRow key={file.spId} file={file} showSupplierName={!isMultiple} {...rowProps} />
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import * as React from 'react';
|
||||
import { IconButton } from '@tnsu/ui-kit';
|
||||
import { CtService } from '../../services/CtService';
|
||||
import { CTFormData } from '../hooks/useCtForm';
|
||||
import { KpFileRef } from './kpCurrency';
|
||||
import styles from './Step2CommercialOffers.module.scss';
|
||||
|
||||
interface Props {
|
||||
file: CTFormData['uploadedKpFiles'][number];
|
||||
// Название поставщика в строке — когда у него один КП (в группе из нескольких оно в заголовке группы).
|
||||
showSupplierName: boolean;
|
||||
service: CtService;
|
||||
currencyOptions: string[];
|
||||
getCurrency: (file: KpFileRef) => string;
|
||||
onCurrencyChange: (spId: number, currencyCode: string) => void;
|
||||
onRemove: (spId: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// Строка «Загруженные КП»: ссылка на файл, валюта КП и «убрать из сравнения».
|
||||
export const UploadedKpRow: React.FC<Props> = ({
|
||||
file,
|
||||
showSupplierName,
|
||||
service,
|
||||
currencyOptions,
|
||||
getCurrency,
|
||||
onCurrencyChange,
|
||||
onRemove,
|
||||
disabled,
|
||||
}) => (
|
||||
<li className={styles.noOfferItem}>
|
||||
{showSupplierName && file.supplierName && <span className={styles.noOfferName}>{file.supplierName}</span>}
|
||||
<a
|
||||
href={service.getKpFileUrl(file.name)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.kpFileLink}
|
||||
>
|
||||
{file.name}
|
||||
</a>
|
||||
{disabled ? (
|
||||
<span className={styles.kpCurrencyCode}>{getCurrency(file)}</span>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
className={styles.kpCurrencySelect}
|
||||
value={getCurrency(file)}
|
||||
onChange={e => onCurrencyChange(file.spId, e.target.value)}
|
||||
// Валюта хранится на колонке поставщика — без него менять нечего.
|
||||
disabled={!file.supplierName}
|
||||
aria-label={`Валюта КП ${file.name}`}
|
||||
>
|
||||
{currencyOptions.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
<IconButton
|
||||
icon="trash"
|
||||
title="Убрать КП из сравнения"
|
||||
onClick={() => onRemove(file.spId)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { groupUploadedKpFiles, removeKpFileFromComparison } from './kpRemoval';
|
||||
import { Supplier, SupplierOffer } from '../types/domain';
|
||||
|
||||
const ARLAN = 'ТОО «Арлан Офис Снаб»';
|
||||
const KALA = 'ТОО «Кала Саплай»';
|
||||
const ARLAN_KZT: Supplier = { id: '263_277', spId: 263, kpFileSpId: 277, name: ARLAN, hasOffer: true };
|
||||
const ARLAN_USD: Supplier = { id: '263_285', spId: 263, kpFileSpId: 285, name: ARLAN, hasOffer: true };
|
||||
const KALA_KZT: Supplier = { id: '264_278', spId: 264, kpFileSpId: 278, name: KALA, hasOffer: true };
|
||||
const UPLOADED = [
|
||||
{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf', supplierName: ARLAN },
|
||||
{ spId: 278, name: 'KP_02_Qala_Supply_KZT.pdf', supplierName: KALA },
|
||||
{ spId: 285, name: 'KP_01_Arlan_Office_USD.pdf', supplierName: ARLAN },
|
||||
];
|
||||
|
||||
function makeOffer(supplierId: string): SupplierOffer {
|
||||
return {
|
||||
id: '', supplierId, positionId: 'p1', pricePerUnit: 1, quantity: 1, currencyCode: 'KZT',
|
||||
pricePerUnitKzt: 1, totalPriceKzt: 1, totalWithExtraKzt: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe('removeKpFileFromComparison', () => {
|
||||
// Загрузили исправленный КП Арлан — старый убирают корзиной: уходит только его колонка.
|
||||
it('убирает колонку, предложения и строку списка только этого КП', () => {
|
||||
const result = removeKpFileFromComparison(
|
||||
{
|
||||
suppliers: [ARLAN_KZT, KALA_KZT, ARLAN_USD],
|
||||
offers: [makeOffer('263_277'), makeOffer('264_278'), makeOffer('263_285')],
|
||||
uploadedKpFiles: UPLOADED,
|
||||
},
|
||||
UPLOADED[0],
|
||||
);
|
||||
|
||||
expect(result.suppliers.map(s => s.id)).toEqual(['264_278', '263_285']);
|
||||
expect(result.offers.map(o => o.supplierId)).toEqual(['264_278', '263_285']);
|
||||
expect(result.uploadedKpFiles.map(f => f.spId)).toEqual([278, 285]);
|
||||
});
|
||||
|
||||
it('старую колонку без файла убирает, когда это последний КП поставщика', () => {
|
||||
const legacy: Supplier = { id: '264', spId: 264, name: KALA, hasOffer: true };
|
||||
|
||||
const result = removeKpFileFromComparison(
|
||||
{ suppliers: [legacy], offers: [makeOffer('264')], uploadedKpFiles: [UPLOADED[1]] },
|
||||
UPLOADED[1],
|
||||
);
|
||||
|
||||
expect(result.suppliers).toEqual([]);
|
||||
expect(result.offers).toEqual([]);
|
||||
});
|
||||
|
||||
it('старую колонку без файла оставляет, пока у поставщика есть другие КП', () => {
|
||||
const legacy: Supplier = { id: '263', spId: 263, name: ARLAN, hasOffer: true };
|
||||
|
||||
const result = removeKpFileFromComparison(
|
||||
{ suppliers: [legacy], offers: [], uploadedKpFiles: [UPLOADED[0], UPLOADED[2]] },
|
||||
UPLOADED[0],
|
||||
);
|
||||
|
||||
expect(result.suppliers).toEqual([legacy]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupUploadedKpFiles', () => {
|
||||
it('группирует КП по поставщику в порядке появления', () => {
|
||||
const groups = groupUploadedKpFiles(UPLOADED);
|
||||
|
||||
expect(groups.map(g => ({ supplierName: g.supplierName, spIds: g.files.map(f => f.spId) }))).toEqual([
|
||||
{ supplierName: ARLAN, spIds: [277, 285] },
|
||||
{ supplierName: KALA, spIds: [278] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('файл без поставщика — отдельной группой', () => {
|
||||
const groups = groupUploadedKpFiles([{ spId: 1, name: 'a.pdf' }, { spId: 2, name: 'b.pdf' }]);
|
||||
|
||||
expect(groups).toHaveLength(2);
|
||||
expect(groups[0].supplierName).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CTFormData } from '../hooks/useCtForm';
|
||||
|
||||
type UploadedKpFile = CTFormData['uploadedKpFiles'][number];
|
||||
type ComparisonData = Pick<CTFormData, 'suppliers' | 'offers' | 'uploadedKpFiles'>;
|
||||
|
||||
// Убрать КП из сравнения (корзина в «Загруженные КП»): его колонку, предложения и строку списка.
|
||||
// Так заменяют исправленный КП — загружают новый, старый убирают. Сам файл остаётся в библиотеке.
|
||||
// Старая колонка без файла (kpFileSpId) общая на все КП поставщика — убираем её, только когда
|
||||
// у поставщика не остаётся других КП.
|
||||
export function removeKpFileFromComparison(data: ComparisonData, file: UploadedKpFile): ComparisonData {
|
||||
const remainingFiles = data.uploadedKpFiles.filter(f => f.spId !== file.spId);
|
||||
const hasFileColumn = data.suppliers.some(s => s.kpFileSpId === file.spId);
|
||||
const supplierHasOtherFiles = remainingFiles.some(f => f.supplierName === file.supplierName);
|
||||
|
||||
const isRemoved = (s: CTFormData['suppliers'][number]): boolean => {
|
||||
if (!s.hasOffer) return false;
|
||||
if (hasFileColumn) return s.kpFileSpId === file.spId;
|
||||
return s.kpFileSpId === undefined && s.name === file.supplierName && !supplierHasOtherFiles;
|
||||
};
|
||||
|
||||
const removedColumnIds = new Set(data.suppliers.filter(isRemoved).map(s => s.id));
|
||||
return {
|
||||
suppliers: data.suppliers.filter(s => !removedColumnIds.has(s.id)),
|
||||
offers: data.offers.filter(o => !removedColumnIds.has(o.supplierId)),
|
||||
uploadedKpFiles: remainingFiles,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UploadedKpGroup {
|
||||
supplierName?: string;
|
||||
files: UploadedKpFile[];
|
||||
}
|
||||
|
||||
// Группы «поставщик → его КП» для списка «Загруженные КП», в порядке первого появления. Несколько КП
|
||||
// одного поставщика стоят рядом — видно, что они сравниваются отдельными колонками.
|
||||
export function groupUploadedKpFiles(files: UploadedKpFile[]): UploadedKpGroup[] {
|
||||
const groups: UploadedKpGroup[] = [];
|
||||
files.forEach(file => {
|
||||
const group = file.supplierName ? groups.find(g => g.supplierName === file.supplierName) : undefined;
|
||||
if (group) {
|
||||
group.files.push(file);
|
||||
return;
|
||||
}
|
||||
groups.push({ supplierName: file.supplierName, files: [file] });
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
import { findOtherKpFilesOfSupplier, removeReplacedColumns } from './kpReplace';
|
||||
import { Supplier, SupplierOffer } from '../types/domain';
|
||||
|
||||
const ARLAN = 'ТОО «Арлан Офис Снаб»';
|
||||
const KZT_COLUMN: Supplier = { id: '263_277', spId: 263, kpFileSpId: 277, name: ARLAN, hasOffer: true };
|
||||
const KALA_COLUMN: Supplier = { id: '264_278', spId: 264, kpFileSpId: 278, name: 'ТОО «Кала Саплай»', hasOffer: true };
|
||||
const UPLOADED = [
|
||||
{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf', supplierName: ARLAN },
|
||||
{ spId: 278, name: 'KP_02_Qala_Supply_KZT.pdf', supplierName: 'ТОО «Кала Саплай»' },
|
||||
];
|
||||
|
||||
function makeOffer(supplierId: string): SupplierOffer {
|
||||
return {
|
||||
id: '', supplierId, positionId: 'p1', pricePerUnit: 1, quantity: 1, currencyCode: 'KZT',
|
||||
pricePerUnitKzt: 1, totalPriceKzt: 1, totalWithExtraKzt: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe('findOtherKpFilesOfSupplier', () => {
|
||||
// Пришёл второй КП Арлан (файл 285) — спросить, заменить ли им KP_01_Arlan_Office_KZT.pdf.
|
||||
it('находит уже загруженный КП того же поставщика', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [KZT_COLUMN, KALA_COLUMN],
|
||||
uploadedKpFiles: UPLOADED,
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf' }]);
|
||||
});
|
||||
|
||||
it('первый КП поставщика — спрашивать нечего', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [KALA_COLUMN],
|
||||
uploadedKpFiles: UPLOADED,
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([]);
|
||||
});
|
||||
|
||||
it('тот же файл, обработанный повторно, не считается другим КП', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [KZT_COLUMN],
|
||||
uploadedKpFiles: UPLOADED,
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 277,
|
||||
});
|
||||
|
||||
expect(other).toEqual([]);
|
||||
});
|
||||
|
||||
it('видит КП того же поставщика, который ещё обрабатывается в этой сессии', () => {
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [],
|
||||
uploadedKpFiles: [],
|
||||
sessionFiles: [{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf', supplierSpId: 263 }],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([{ spId: 277, name: 'KP_01_Arlan_Office_KZT.pdf' }]);
|
||||
});
|
||||
|
||||
it('старая колонка без файла КП тоже считается загруженным КП', () => {
|
||||
const legacy: Supplier = { id: '263', spId: 263, name: ARLAN, hasOffer: true };
|
||||
|
||||
const other = findOtherKpFilesOfSupplier({
|
||||
columns: [legacy],
|
||||
uploadedKpFiles: [],
|
||||
sessionFiles: [],
|
||||
supplierSpId: 263,
|
||||
fileSpId: 285,
|
||||
});
|
||||
|
||||
expect(other).toEqual([{ spId: undefined, name: ARLAN }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeReplacedColumns', () => {
|
||||
it('«Заменить» убирает прежние колонки поставщика, их предложения и файлы', () => {
|
||||
const usdColumn: Supplier = { id: '263_285', spId: 263, kpFileSpId: 285, name: ARLAN, hasOffer: true };
|
||||
|
||||
const result = removeReplacedColumns(
|
||||
{
|
||||
suppliers: [KZT_COLUMN, KALA_COLUMN, usdColumn],
|
||||
offers: [makeOffer('263_277'), makeOffer('264_278'), makeOffer('263_285')],
|
||||
uploadedKpFiles: [...UPLOADED, { spId: 285, name: 'KP_01_Arlan_Office_USD.pdf', supplierName: ARLAN }],
|
||||
},
|
||||
263,
|
||||
285,
|
||||
);
|
||||
|
||||
expect(result.suppliers.map(s => s.id)).toEqual(['264_278', '263_285']);
|
||||
expect(result.offers.map(o => o.supplierId)).toEqual(['264_278', '263_285']);
|
||||
expect(result.uploadedKpFiles.map(f => f.spId)).toEqual([278, 285]);
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Supplier, SupplierOffer } from '../types/domain';
|
||||
import { CTFormData } from '../hooks/useCtForm';
|
||||
|
||||
// Второй КП того же поставщика: пользователь выбирает, заменить им прежний (исправленный КП, тот же
|
||||
// файл под другим именем) или добавить отдельным предложением (напр. другая валюта). Сама система
|
||||
// эти случаи не различает — поставщик определяется только по названию.
|
||||
export type KpReplaceChoice = 'replace' | 'separate';
|
||||
|
||||
// Уже загруженный КП поставщика — для текста вопроса. spId нет у старых колонок без файла КП.
|
||||
export interface ExistingKpFile {
|
||||
spId?: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FindOtherKpFilesArgs {
|
||||
columns: Supplier[];
|
||||
uploadedKpFiles: CTFormData['uploadedKpFiles'];
|
||||
// КП этой сессии, которые ещё обрабатываются: их колонки появятся только после обработки.
|
||||
sessionFiles: Array<{ spId?: number; name: string; supplierSpId?: number }>;
|
||||
supplierSpId: number;
|
||||
fileSpId: number;
|
||||
}
|
||||
|
||||
// Другие КП того же поставщика в этой СТ (кроме только что обработанного файла).
|
||||
export function findOtherKpFilesOfSupplier(args: FindOtherKpFilesArgs): ExistingKpFile[] {
|
||||
const { columns, uploadedKpFiles, sessionFiles, supplierSpId, fileSpId } = args;
|
||||
const found: ExistingKpFile[] = [];
|
||||
const seenFileIds = new Set<number>([fileSpId]);
|
||||
|
||||
columns
|
||||
.filter(c => c.hasOffer && c.spId === supplierSpId)
|
||||
.forEach(c => {
|
||||
if (c.kpFileSpId === undefined) {
|
||||
// Старая колонка без файла КП (данные до появления нескольких КП у поставщика).
|
||||
found.push({ spId: undefined, name: c.name });
|
||||
return;
|
||||
}
|
||||
if (seenFileIds.has(c.kpFileSpId)) return;
|
||||
seenFileIds.add(c.kpFileSpId);
|
||||
const uploaded = uploadedKpFiles.find(u => u.spId === c.kpFileSpId);
|
||||
found.push({ spId: c.kpFileSpId, name: uploaded?.name ?? c.name });
|
||||
});
|
||||
|
||||
sessionFiles
|
||||
.filter(f => f.supplierSpId === supplierSpId && f.spId !== undefined && !seenFileIds.has(f.spId))
|
||||
.forEach(f => {
|
||||
seenFileIds.add(f.spId as number);
|
||||
found.push({ spId: f.spId, name: f.name });
|
||||
});
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
type ReplaceableData = Pick<CTFormData, 'suppliers' | 'offers' | 'uploadedKpFiles'>;
|
||||
|
||||
// «Заменить»: убираем прежние колонки поставщика (все, кроме колонки нового файла КП), их предложения
|
||||
// и записи в списке «Загруженные КП». Сам файл остаётся в библиотеке — удаляются только данные СТ.
|
||||
export function removeReplacedColumns(
|
||||
data: ReplaceableData,
|
||||
supplierSpId: number,
|
||||
keepFileSpId: number,
|
||||
): ReplaceableData {
|
||||
const isReplaced = (s: Supplier): boolean =>
|
||||
s.hasOffer === true && s.spId === supplierSpId && s.kpFileSpId !== keepFileSpId;
|
||||
|
||||
const replacedColumns = data.suppliers.filter(isReplaced);
|
||||
const replacedColumnIds = new Set(replacedColumns.map(s => s.id));
|
||||
const replacedFileIds = new Set(
|
||||
replacedColumns.map(s => s.kpFileSpId).filter((id): id is number => id !== undefined),
|
||||
);
|
||||
|
||||
return {
|
||||
suppliers: data.suppliers.filter(s => !isReplaced(s)),
|
||||
offers: data.offers.filter((o: SupplierOffer) => !replacedColumnIds.has(o.supplierId)),
|
||||
uploadedKpFiles: data.uploadedKpFiles.filter(f => !replacedFileIds.has(f.spId)),
|
||||
};
|
||||
}
|
||||
+7
-63
@@ -4,8 +4,6 @@ import { CTFormData } from '../hooks/useCtForm';
|
||||
import { KpFile, FlowPositionResult, POLL_INTERVAL_MS, MAX_POLL_RETRIES } from './kpTypes';
|
||||
import { buildKpSuppliersAndOffers } from './step2Persist';
|
||||
import { buildKpOffersToSave } from './kpOfferRecords';
|
||||
import { KpReplaceChoice, findOtherKpFilesOfSupplier, removeReplacedColumns } from './kpReplace';
|
||||
import { KpReplaceQuestion, useKpReplaceQuestion } from './useKpReplaceQuestion';
|
||||
|
||||
let localIdCounter = 0;
|
||||
|
||||
@@ -16,10 +14,9 @@ export interface UseKpProcessingResult {
|
||||
handleFilesAdded: (files: File[]) => Promise<void>;
|
||||
handleFileRemove: (localId: string) => void;
|
||||
handleCurrencyChange: (localId: string, currency: string) => void;
|
||||
// Убрать файл из очереди загрузки по SP Id (КП убрали из сравнения): иначе он снова соберётся в колонку.
|
||||
removeFileBySpId: (fileSpId: number) => void;
|
||||
isProcessing: boolean;
|
||||
// Вопрос «заменить / добавить отдельно» для второго КП поставщика (undefined — окна нет).
|
||||
replaceQuestion: KpReplaceQuestion | undefined;
|
||||
answerReplaceQuestion: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
// Конвейер обработки КП (Шаг 2): загрузка в библиотеку → извлечение текста (Word) → поллинг Syntex →
|
||||
@@ -36,7 +33,6 @@ export function useKpProcessing(
|
||||
const mountedRef = useRef(true);
|
||||
const formDataRef = useRef(formData);
|
||||
const kpFilesRef = useRef<KpFile[]>([]);
|
||||
const replaceQuestion = useKpReplaceQuestion();
|
||||
|
||||
useEffect(() => { formDataRef.current = formData; }, [formData]);
|
||||
useEffect(() => { kpFilesRef.current = kpFiles; }, [kpFiles]);
|
||||
@@ -75,50 +71,6 @@ export function useKpProcessing(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kpFiles]);
|
||||
|
||||
// Другие КП этого поставщика в СТ — если есть, спрашиваем «заменить или добавить отдельно».
|
||||
const askReplaceChoice = async (
|
||||
localId: string,
|
||||
fileName: string,
|
||||
fileSpId: number,
|
||||
supplierSpId: number,
|
||||
supplierName: string,
|
||||
): Promise<KpReplaceChoice> => {
|
||||
const fd = formDataRef.current;
|
||||
const existing = findOtherKpFilesOfSupplier({
|
||||
columns: fd.suppliers ?? [],
|
||||
uploadedKpFiles: fd.uploadedKpFiles ?? [],
|
||||
sessionFiles: kpFilesRef.current
|
||||
.filter(f => f.localId !== localId && f.status !== 'error')
|
||||
.map(f => ({ spId: f.spId, name: f.file.name, supplierSpId: f.supplierSpId })),
|
||||
supplierSpId,
|
||||
fileSpId,
|
||||
});
|
||||
if (existing.length === 0) return 'separate';
|
||||
return replaceQuestion.ask({ fileName, supplierName, existing });
|
||||
};
|
||||
|
||||
// «Заменить»: прежние КП поставщика уходят из сравнения — из стейта сразу, из SP (в корзину) после
|
||||
// записи предложений нового КП. Стейт обновляем до статуса done нового файла: иначе инкрементальный
|
||||
// persist ниже успеет собрать колонки по ещё не очищенным formData.
|
||||
const applyReplace = async (supplierSpId: number, keepFileSpId: number): Promise<void> => {
|
||||
const fd = formDataRef.current;
|
||||
setKpFiles(prev => prev.filter(f => f.supplierSpId !== supplierSpId || f.spId === keepFileSpId));
|
||||
onSave(removeReplacedColumns(
|
||||
{ suppliers: fd.suppliers ?? [], offers: fd.offers ?? [], uploadedKpFiles: fd.uploadedKpFiles ?? [] },
|
||||
supplierSpId,
|
||||
keepFileSpId,
|
||||
));
|
||||
const positionSpIds = [...fd.selectedPositions, ...(fd.step3ExtraPositions ?? [])]
|
||||
.map(p => p.spId)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
try {
|
||||
await service.recycleOtherKpOffers(supplierSpId, positionSpIds, keepFileSpId);
|
||||
} catch (err) {
|
||||
// Не критично: «Сохранить» на Шаге 2/3 перезапишет предложения позиций по текущей таблице.
|
||||
console.warn('[КП] не удалось убрать предложения прежнего КП:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const patchFile = (localId: string, patch: Partial<KpFile>): void => {
|
||||
if (!mountedRef.current) return;
|
||||
setKpFiles(prev => prev.map(f => f.localId === localId ? { ...f, ...patch } : f));
|
||||
@@ -157,11 +109,9 @@ export function useKpProcessing(
|
||||
|
||||
// If supplier wasn't set by Syntex, use supplier name from flow result
|
||||
let resolvedSupplierSpId = supplierSpId;
|
||||
let resolvedSupplierName = kpFilesRef.current.find(f => f.localId === localId)?.supplierName ?? '';
|
||||
if (!resolvedSupplierSpId) {
|
||||
const supplierName = collectedResults.find(r => r.flowResult.supplier)?.flowResult.supplier;
|
||||
if (supplierName) {
|
||||
resolvedSupplierName = supplierName;
|
||||
try {
|
||||
resolvedSupplierSpId = await service.saveSupplierWithOffer(supplierName);
|
||||
patchFile(localId, { supplierName, supplierSpId: resolvedSupplierSpId });
|
||||
@@ -171,11 +121,6 @@ export function useKpProcessing(
|
||||
}
|
||||
}
|
||||
|
||||
// Второй КП того же поставщика — ждём выбора пользователя до записи в SP.
|
||||
const replaceChoice: KpReplaceChoice = resolvedSupplierSpId
|
||||
? await askReplaceChoice(localId, fileName, fileSpId, resolvedSupplierSpId, resolvedSupplierName)
|
||||
: 'separate';
|
||||
|
||||
// Предложения пишем после того, как поставщик определён: Syntex отключён, и поставщика чаще
|
||||
// называет сам поток — внутри цикла по позициям его ещё нет, и записи раньше не создавались вовсе.
|
||||
const offersToSave = buildKpOffersToSave(collectedResults, positions, resolvedSupplierSpId, fileSpId);
|
||||
@@ -186,10 +131,6 @@ export function useKpProcessing(
|
||||
})
|
||||
));
|
||||
|
||||
if (replaceChoice === 'replace' && resolvedSupplierSpId) {
|
||||
await applyReplace(resolvedSupplierSpId, fileSpId);
|
||||
}
|
||||
|
||||
// БИН поставщика (для Adata) — берём первый результат из позиций, где он вообще пришёл
|
||||
const supplierBIN = collectedResults.find(r => r.flowResult.supplierBIN)?.flowResult.supplierBIN;
|
||||
if (supplierBIN && resolvedSupplierSpId) {
|
||||
@@ -332,6 +273,10 @@ export function useKpProcessing(
|
||||
setKpFiles(prev => prev.filter(f => f.localId !== localId));
|
||||
};
|
||||
|
||||
const removeFileBySpId = (fileSpId: number): void => {
|
||||
setKpFiles(prev => prev.filter(f => f.spId !== fileSpId));
|
||||
};
|
||||
|
||||
const handleCurrencyChange = (localId: string, currency: string): void => {
|
||||
setKpFiles(prev => prev.map(f => f.localId === localId ? { ...f, currencyCode: currency } : f));
|
||||
};
|
||||
@@ -345,8 +290,7 @@ export function useKpProcessing(
|
||||
handleFilesAdded,
|
||||
handleFileRemove,
|
||||
handleCurrencyChange,
|
||||
removeFileBySpId,
|
||||
isProcessing,
|
||||
replaceQuestion: replaceQuestion.current,
|
||||
answerReplaceQuestion: replaceQuestion.answer,
|
||||
};
|
||||
}
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ExistingKpFile, KpReplaceChoice } from './kpReplace';
|
||||
|
||||
// Вопрос «заменить или добавить отдельно» по одному обработанному КП.
|
||||
export interface KpReplaceQuestion {
|
||||
fileName: string;
|
||||
supplierName: string;
|
||||
existing: ExistingKpFile[];
|
||||
}
|
||||
|
||||
interface PendingQuestion {
|
||||
question: KpReplaceQuestion;
|
||||
resolve: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
export interface UseKpReplaceQuestionResult {
|
||||
// Вопрос, который сейчас показан (undefined — окна нет).
|
||||
current: KpReplaceQuestion | undefined;
|
||||
ask: (question: KpReplaceQuestion) => Promise<KpReplaceChoice>;
|
||||
answer: (choice: KpReplaceChoice) => void;
|
||||
}
|
||||
|
||||
// Очередь вопросов: несколько КП обрабатываются параллельно, а окно одно — спрашиваем по очереди.
|
||||
// Обработка КП ждёт ответа (Promise) перед записью предложений в SP.
|
||||
// Ушли с шага, не ответив — считаем «добавить отдельно»: так ничего не удаляется.
|
||||
export function useKpReplaceQuestion(): UseKpReplaceQuestionResult {
|
||||
const queueRef = useRef<PendingQuestion[]>([]);
|
||||
const [current, setCurrent] = useState<KpReplaceQuestion | undefined>(undefined);
|
||||
|
||||
const showNext = useCallback((): void => {
|
||||
setCurrent(queueRef.current[0]?.question);
|
||||
}, []);
|
||||
|
||||
const ask = useCallback((question: KpReplaceQuestion): Promise<KpReplaceChoice> => {
|
||||
return new Promise<KpReplaceChoice>(resolve => {
|
||||
queueRef.current.push({ question, resolve });
|
||||
if (queueRef.current.length === 1) showNext();
|
||||
});
|
||||
}, [showNext]);
|
||||
|
||||
const answer = useCallback((choice: KpReplaceChoice): void => {
|
||||
const pending = queueRef.current.shift();
|
||||
pending?.resolve(choice);
|
||||
showNext();
|
||||
}, [showNext]);
|
||||
|
||||
useEffect(() => () => {
|
||||
queueRef.current.forEach(p => p.resolve('separate'));
|
||||
queueRef.current = [];
|
||||
}, []);
|
||||
|
||||
return { current, ask, answer };
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
writeRoute,
|
||||
} from "../../../shared/approval";
|
||||
import {
|
||||
findOtherKpOfferIds,
|
||||
findKpFileOfferIds,
|
||||
findSupersededOfferIds,
|
||||
keepLatestOfferRecords,
|
||||
parseOfferItemIndex,
|
||||
@@ -1619,29 +1619,24 @@ export class CtService {
|
||||
});
|
||||
}
|
||||
|
||||
// «Заменить» прежний КП поставщика новым (Шаг 2): записи поставщика по позициям СТ, кроме записей
|
||||
// нового файла, уходят в корзину. Вызывается после записи предложений нового КП — при сбое записи
|
||||
// прежнее предложение останется.
|
||||
async recycleOtherKpOffers(
|
||||
supplierSpId: number,
|
||||
positionSpIds: number[],
|
||||
keepFileSpId: number,
|
||||
): Promise<void> {
|
||||
// КП убрали из сравнения (Шаг 2, корзина в «Загруженные КП»): записи этого файла по позициям СТ
|
||||
// уходят в корзину. Сам файл остаётся в библиотеке. Старые записи без offerFileId не найти —
|
||||
// их перезапишет «Сохранить» по текущей таблице.
|
||||
async recycleKpFileOffers(fileSpId: number, positionSpIds: number[]): Promise<void> {
|
||||
if (positionSpIds.length === 0) return;
|
||||
const positionFilter = positionSpIds.map((id) => `positionId eq ${id}`).join(" or ");
|
||||
const url =
|
||||
`${this._li(LISTS.OFFERS)}?$select=Id,supplierId,offerFileId` +
|
||||
`&$filter=supplierId eq ${supplierSpId} and (${positionFilter})&$top=5000`;
|
||||
`${this._li(LISTS.OFFERS)}?$select=Id,offerFileId` +
|
||||
`&$filter=offerFileId eq ${fileSpId} and (${positionFilter})&$top=5000`;
|
||||
const res = await this.http.get(url, SPHttpClient.configurations.v1);
|
||||
if (!res.ok) throw new Error(`Ошибка чтения прежних предложений: ${res.status}`);
|
||||
if (!res.ok) throw new Error(`Ошибка чтения предложений КП: ${res.status}`);
|
||||
const data = await res.json();
|
||||
const records = ((data.value ?? []) as Array<Record<string, unknown>>).map((item) => ({
|
||||
id: Number(item.Id),
|
||||
supplierSpId: Number(item.supplierId),
|
||||
offerFileSpId: item.offerFileId ? Number(item.offerFileId) : undefined,
|
||||
}));
|
||||
// MERGE на этом списке блокирует ресивер — убираем через recycle(), как в saveOffers.
|
||||
await this._runChunked(findOtherKpOfferIds(records, supplierSpId, keepFileSpId), 20, async (id) => {
|
||||
await this._runChunked(findKpFileOfferIds(records, fileSpId), 20, async (id) => {
|
||||
await this.http
|
||||
.post(`${this._liById(LISTS.OFFERS, id)}/recycle()`, SPHttpClient.configurations.v1, {})
|
||||
.catch(() => undefined /* уже в корзине / не найден — игнорируем */);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IOfferRecordKey, findOtherKpOfferIds, findSupersededOfferIds, findSupplierOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
||||
import { IOfferRecordKey, findKpFileOfferIds, findSupersededOfferIds, findSupplierOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
||||
|
||||
interface ITestRecord extends IOfferRecordKey {
|
||||
price: number;
|
||||
@@ -150,15 +150,15 @@ describe('несколько КП одного поставщика (offerFileSp
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOtherKpOfferIds («Заменить» прежний КП новым)', () => {
|
||||
it('возвращает записи других КП поставщика и старые записи без файла', () => {
|
||||
describe('findKpFileOfferIds (КП убрали из сравнения)', () => {
|
||||
it('возвращает записи только этого файла КП', () => {
|
||||
const records = [
|
||||
makeRecord({ id: 10, supplierSpId: 263, offerFileSpId: 277 }),
|
||||
makeRecord({ id: 11, supplierSpId: 263 }),
|
||||
makeRecord({ id: 12, supplierSpId: 263, offerFileSpId: 285 }),
|
||||
makeRecord({ id: 13, supplierSpId: 264, offerFileSpId: 278 }),
|
||||
makeRecord({ id: 13, supplierSpId: 264, offerFileSpId: 277 }),
|
||||
];
|
||||
|
||||
expect(findOtherKpOfferIds(records, 263, 285)).toEqual([10, 11]);
|
||||
expect(findKpFileOfferIds(records, 277)).toEqual([10, 13]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,16 +46,12 @@ export function findSupplierOfferIds(
|
||||
.map(record => record.id);
|
||||
}
|
||||
|
||||
// «Заменить» прежний КП поставщика новым: все записи поставщика, кроме записей нового файла КП,
|
||||
// включая старые записи без файла.
|
||||
export function findOtherKpOfferIds(
|
||||
records: Array<Pick<IOfferRecordKey, 'id' | 'supplierSpId' | 'offerFileSpId'>>,
|
||||
supplierSpId: number,
|
||||
keepFileSpId: number,
|
||||
// Записи одного файла КП — когда КП убирают из сравнения (корзина в «Загруженные КП»).
|
||||
export function findKpFileOfferIds(
|
||||
records: Array<Pick<IOfferRecordKey, 'id' | 'offerFileSpId'>>,
|
||||
fileSpId: number,
|
||||
): number[] {
|
||||
return records
|
||||
.filter(record => record.supplierSpId === supplierSpId && record.offerFileSpId !== keepFileSpId)
|
||||
.map(record => record.id);
|
||||
return records.filter(record => record.offerFileSpId === fileSpId).map(record => record.id);
|
||||
}
|
||||
|
||||
// Какие записи вытеснила только что сохранённая: прежние записи того же поставщика и того же файла КП
|
||||
|
||||
Reference in New Issue
Block a user