feat(ct): поставщики без КП — списком под таблицей сравнения, в т.ч. у согласующего
Поставщиков без КП убрали из колонок (64973a2), но взамен нигде не показывали. Теперь на
шаге 3 под «Дополнительной информацией» — список «Поставщики, не предоставившие КП» с
комментарием. «Не получены КП» сохраняются с привязкой к СТ (колонка CT была в списке, но не
заполнялась, а дедуп шёл по названию на все СТ) и синхронизируются по «Сохранить» на шагах
2 и 3 — удалённые убираются; по ссылке согласующий видит список из SP. Комментарий больше
не теряется при возврате на шаг 2.
This commit is contained in:
@@ -10,6 +10,7 @@ import { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTabl
|
|||||||
import { Step4Approval } from './Step4Approval/Step4Approval';
|
import { Step4Approval } from './Step4Approval/Step4Approval';
|
||||||
import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns';
|
import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns';
|
||||||
import { buildOffersFromSpRecords } from './utils/spOffers';
|
import { buildOffersFromSpRecords } from './utils/spOffers';
|
||||||
|
import { noOfferRecordsToSuppliers, withNoOfferSuppliers } from './utils/noOfferSuppliers';
|
||||||
import styles from './CtCreateForm.module.scss';
|
import styles from './CtCreateForm.module.scss';
|
||||||
import '@tnsu/ui-kit/styles.css';
|
import '@tnsu/ui-kit/styles.css';
|
||||||
|
|
||||||
@@ -65,6 +66,13 @@ const CtCreateForm: React.FC<ICtCreateFormProps> = ({ context, userDisplayName,
|
|||||||
]).then(async ([ct, approvalMatrix, positions]) => {
|
]).then(async ([ct, approvalMatrix, positions]) => {
|
||||||
const positionSpIds = positions.map(p => p.spId).filter((id): id is number => id !== undefined);
|
const positionSpIds = positions.map(p => p.spId).filter((id): id is number => id !== undefined);
|
||||||
const rawOffers = await service.loadFullOffersForPositions(positionSpIds);
|
const rawOffers = await service.loadFullOffersForPositions(positionSpIds);
|
||||||
|
// Поставщики без КП этой СТ — для списка под таблицей сравнения (согласующий по ссылке).
|
||||||
|
const noOfferFromSp = noOfferRecordsToSuppliers(
|
||||||
|
await service.loadNoOfferSuppliers(initialCtId).catch(err => {
|
||||||
|
console.warn('[CtCreateForm] не удалось загрузить поставщиков без КП:', err);
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Build supplierSpId → offerId map from session uploadedKpFiles.
|
// Build supplierSpId → offerId map from session uploadedKpFiles.
|
||||||
// Лукап offerId через REST не читается (500), поэтому сначала берём из сессии, а без сессии —
|
// Лукап offerId через REST не читается (500), поэтому сначала берём из сессии, а без сессии —
|
||||||
@@ -152,7 +160,7 @@ const CtCreateForm: React.FC<ICtCreateFormProps> = ({ context, userDisplayName,
|
|||||||
...(isNotificationLink || formData.selectedPositions.length === 0 ? { selectedPositions: positions } : {}),
|
...(isNotificationLink || formData.selectedPositions.length === 0 ? { selectedPositions: positions } : {}),
|
||||||
// Preserve session suppliers and offers — SP data may be incomplete or stale vs. unsaved edits.
|
// Preserve session suppliers and offers — SP data may be incomplete or stale vs. unsaved edits.
|
||||||
// Only use SP-derived data when session has none, or when opened via notification link (always fresh).
|
// Only use SP-derived data when session has none, or when opened via notification link (always fresh).
|
||||||
...(formData.suppliers.length === 0 ? { suppliers: spSuppliers } : {}),
|
suppliers: withNoOfferSuppliers(formData.suppliers.length === 0 ? spSuppliers : formData.suppliers, noOfferFromSp),
|
||||||
...(isNotificationLink || formData.offers.length === 0 ? { offers } : {}),
|
...(isNotificationLink || formData.offers.length === 0 ? { offers } : {}),
|
||||||
...(formData.currencyRates.length === 0 && spCurrencyRates.length > 0 ? { currencyRates: spCurrencyRates } : {}),
|
...(formData.currencyRates.length === 0 && spCurrencyRates.length > 0 ? { currencyRates: spCurrencyRates } : {}),
|
||||||
...(isNotificationLink ? { approvalSent: true } : {}),
|
...(isNotificationLink ? { approvalSent: true } : {}),
|
||||||
|
|||||||
+2
-1
@@ -45,7 +45,7 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
|||||||
const [noOfferSuppliers, setNoOfferSuppliers] = useState<NoOfferEntry[]>(() =>
|
const [noOfferSuppliers, setNoOfferSuppliers] = useState<NoOfferEntry[]>(() =>
|
||||||
(formData.suppliers ?? [])
|
(formData.suppliers ?? [])
|
||||||
.filter(s => !s.hasOffer)
|
.filter(s => !s.hasOffer)
|
||||||
.map(s => ({ localId: s.id, name: s.name, comment: '' })),
|
.map(s => ({ localId: s.id, name: s.name, comment: s.comment ?? '' })),
|
||||||
);
|
);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | undefined>(undefined);
|
const [saveError, setSaveError] = useState<string | undefined>(undefined);
|
||||||
@@ -70,6 +70,7 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
|||||||
id: s.localId,
|
id: s.localId,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
hasOffer: false,
|
hasOffer: false,
|
||||||
|
comment: s.comment,
|
||||||
}));
|
}));
|
||||||
onSave({ suppliers: [...withOffer, ...withoutOffer] });
|
onSave({ suppliers: [...withOffer, ...withoutOffer] });
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-5
@@ -3,6 +3,7 @@ import { CtService } from '../../services/CtService';
|
|||||||
import { CTFormData } from '../hooks/useCtForm';
|
import { CTFormData } from '../hooks/useCtForm';
|
||||||
import { KpFile, NoOfferEntry } from './kpTypes';
|
import { KpFile, NoOfferEntry } from './kpTypes';
|
||||||
import { buildSupplierColumnId } from '../utils/supplierColumns';
|
import { buildSupplierColumnId } from '../utils/supplierColumns';
|
||||||
|
import { getNoOfferSuppliers } from '../utils/noOfferSuppliers';
|
||||||
|
|
||||||
export interface PersistStep2Args {
|
export interface PersistStep2Args {
|
||||||
service: CtService;
|
service: CtService;
|
||||||
@@ -76,16 +77,13 @@ export async function persistStep2(args: PersistStep2Args): Promise<Partial<CTFo
|
|||||||
savedRates = await service.saveCurrencyRates(currencyRates);
|
savedRates = await service.saveCurrencyRates(currencyRates);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const s of noOfferSuppliers) {
|
|
||||||
await service.saveNoOfferSupplierItem(s.name, s.comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { suppliers: newSuppliersWithOffer, offers: newOffers } = buildKpSuppliersAndOffers(kpFiles, savedRates);
|
const { suppliers: newSuppliersWithOffer, offers: newOffers } = buildKpSuppliersAndOffers(kpFiles, savedRates);
|
||||||
|
|
||||||
const suppliersWithoutOffer: Supplier[] = noOfferSuppliers.map(s => ({
|
const suppliersWithoutOffer: Supplier[] = noOfferSuppliers.map(s => ({
|
||||||
id: s.localId,
|
id: s.localId,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
hasOffer: false,
|
hasOffer: false,
|
||||||
|
comment: s.comment,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Persist offers with currency data to SP so shared links show correct currency columns.
|
// Persist offers with currency data to SP so shared links show correct currency columns.
|
||||||
@@ -128,9 +126,15 @@ export async function persistStep2(args: PersistStep2Args): Promise<Partial<CTFo
|
|||||||
// API, см. план миграции согласования) маршрут — это планируемый resolve, который Step4Approval
|
// API, см. план миграции согласования) маршрут — это планируемый resolve, который Step4Approval
|
||||||
// резолвит «на лету» при открытии шага (CtService.getPlannedApprovalMatrix), писать в SP заранее не нужно.
|
// резолвит «на лету» при открытии шага (CtService.getPlannedApprovalMatrix), писать в SP заранее не нужно.
|
||||||
|
|
||||||
|
// «Не получены КП» — с привязкой к СТ: по ссылке согласующий видит список под таблицей сравнения.
|
||||||
|
const allNoOfferSuppliers = [...preservedNoOfferSuppliers, ...suppliersWithoutOffer];
|
||||||
|
if (formData.ct) {
|
||||||
|
await service.syncNoOfferSuppliers(formData.ct.id, getNoOfferSuppliers(allNoOfferSuppliers));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currencyRates: savedRates,
|
currencyRates: savedRates,
|
||||||
suppliers: [...mergedSuppliersWithOffer, ...preservedNoOfferSuppliers, ...suppliersWithoutOffer],
|
suppliers: [...mergedSuppliersWithOffer, ...allNoOfferSuppliers],
|
||||||
offers: mergedOffers,
|
offers: mergedOffers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { NoOfferSupplierEntry } from '../utils/noOfferSuppliers';
|
||||||
|
import styles from './Step3ComparisonTable.module.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entries: NoOfferSupplierEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поставщики, не предоставившие КП (Шаг 2), — списком под таблицей сравнения. Колонками их не
|
||||||
|
// показываем: сравнивать у них нечего, а список нужен согласующим — к кому обращались.
|
||||||
|
export const NoOfferSuppliersList: React.FC<Props> = ({ entries }) => {
|
||||||
|
if (entries.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className={styles.criteriaSection}>
|
||||||
|
<h4 className={styles.criteriaSectionTitle}>Поставщики, не предоставившие КП</h4>
|
||||||
|
<ul className={styles.noOfferSupplierList}>
|
||||||
|
{entries.map(entry => (
|
||||||
|
<li key={entry.name} className={styles.noOfferSupplierItem}>
|
||||||
|
<span className={styles.noOfferSupplierName}>{entry.name}</span>
|
||||||
|
{entry.comment && <span className={styles.noOfferSupplierComment}>{entry.comment}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+26
@@ -592,6 +592,32 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// «Поставщики, не предоставившие КП» под таблицей (NoOfferSuppliersList) — в рамке как «Доп. информация».
|
||||||
|
.noOfferSupplierList {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noOfferSupplierItem {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noOfferSupplierName {
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noOfferSupplierComment {
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
.criteriaTable {
|
.criteriaTable {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+4
@@ -12,6 +12,8 @@ import { AiCommentBlock } from './AiCommentBlock';
|
|||||||
import { useStep3Matrix } from './useStep3Matrix';
|
import { useStep3Matrix } from './useStep3Matrix';
|
||||||
import { ComparisonMatrix } from './ComparisonMatrix';
|
import { ComparisonMatrix } from './ComparisonMatrix';
|
||||||
import { CriteriaTable } from './CriteriaTable';
|
import { CriteriaTable } from './CriteriaTable';
|
||||||
|
import { NoOfferSuppliersList } from './NoOfferSuppliersList';
|
||||||
|
import { getNoOfferSuppliers } from '../utils/noOfferSuppliers';
|
||||||
import { useAdataCheck } from './useAdataCheck';
|
import { useAdataCheck } from './useAdataCheck';
|
||||||
import { useRecommendedPick } from './useRecommendedPick';
|
import { useRecommendedPick } from './useRecommendedPick';
|
||||||
import { VendorPickerDialog } from './VendorPickerDialog';
|
import { VendorPickerDialog } from './VendorPickerDialog';
|
||||||
@@ -171,6 +173,8 @@ export const Step3ComparisonTable: React.FC<Props> = ({
|
|||||||
|
|
||||||
<CriteriaTable matrix={matrix} />
|
<CriteriaTable matrix={matrix} />
|
||||||
|
|
||||||
|
<NoOfferSuppliersList entries={getNoOfferSuppliers(formData.suppliers)} />
|
||||||
|
|
||||||
<AiCommentBlock
|
<AiCommentBlock
|
||||||
comment={ai.aiComment}
|
comment={ai.aiComment}
|
||||||
decodedComment={ai.decodedAiComment}
|
decodedComment={ai.decodedAiComment}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ExtraSupplier, ExtraPosition, PositionRow, CellEdit, EditableOffers, Cr
|
|||||||
import { applyCreatedIds, buildOfferKey, computeOffer } from './matrixHelpers';
|
import { applyCreatedIds, buildOfferKey, computeOffer } from './matrixHelpers';
|
||||||
import { IVendorRecord } from '../../services/VendorDatabaseService';
|
import { IVendorRecord } from '../../services/VendorDatabaseService';
|
||||||
import { buildColumnDisplayNames, buildColumnLabels, findColumnForRecord } from '../utils/supplierColumns';
|
import { buildColumnDisplayNames, buildColumnLabels, findColumnForRecord } from '../utils/supplierColumns';
|
||||||
|
import { getNoOfferSuppliers } from '../utils/noOfferSuppliers';
|
||||||
|
|
||||||
let extraIdCounter = 0;
|
let extraIdCounter = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export interface Supplier {
|
|||||||
inn?: string; // UI only — not stored in SP
|
inn?: string; // UI only — not stored in SP
|
||||||
bin?: string; // БИН из флоу (Adata) — хранится в SP-поле 'bin'
|
bin?: string; // БИН из флоу (Adata) — хранится в SP-поле 'bin'
|
||||||
hasOffer?: boolean; // determines target list: Поставщики vs Не получены КП
|
hasOffer?: boolean; // determines target list: Поставщики vs Не получены КП
|
||||||
|
comment?: string; // только у поставщика без КП (hasOffer: false) — почему нет КП
|
||||||
status?: string;
|
status?: string;
|
||||||
currency?: string; // currency code for KP (KZT by default); stored here so Step 3 can show correct columns even if offers are empty
|
currency?: string; // currency code for KP (KZT by default); stored here so Step 3 can show correct columns even if offers are empty
|
||||||
adataData?: AdataCompanyInfo; // кэш последней проверки Adata — SP-поле 'adataData' (JSON)
|
adataData?: AdataCompanyInfo; // кэш последней проверки Adata — SP-поле 'adataData' (JSON)
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { getNoOfferSuppliers, noOfferRecordsToSuppliers, planNoOfferSync, withNoOfferSuppliers } from './noOfferSuppliers';
|
||||||
|
import { Supplier } from '../types/domain';
|
||||||
|
|
||||||
|
describe('planNoOfferSync («Не получены КП» по одной СТ)', () => {
|
||||||
|
const existing = [
|
||||||
|
{ id: 1, name: 'ТОО БЕЗ КП ТЕСТ', comment: 'НЕ ТРОГАТЬ, ТЕСТ!!!!!' },
|
||||||
|
{ id: 2, name: 'ТОО Asia Techno', comment: 'не работают с маленьким объемом' },
|
||||||
|
];
|
||||||
|
|
||||||
|
it('новый поставщик — создать', () => {
|
||||||
|
const plan = planNoOfferSync(existing, [
|
||||||
|
...existing.map(e => ({ name: e.name, comment: e.comment })),
|
||||||
|
{ name: 'ТОО «Новый»', comment: '' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(plan.toCreate).toEqual([{ name: 'ТОО «Новый»', comment: '' }]);
|
||||||
|
expect(plan.toRemove).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удалили на шаге 2 — должен уйти и из SP, иначе согласующий по ссылке увидит его снова.
|
||||||
|
it('удалённый на шаге 2 — убрать', () => {
|
||||||
|
const plan = planNoOfferSync(existing, [{ name: 'ТОО БЕЗ КП ТЕСТ', comment: 'НЕ ТРОГАТЬ, ТЕСТ!!!!!' }]);
|
||||||
|
|
||||||
|
expect(plan.toRemove).toEqual([2]);
|
||||||
|
expect(plan.toCreate).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('изменённый комментарий — пересоздать запись', () => {
|
||||||
|
const plan = planNoOfferSync(existing, [
|
||||||
|
{ name: 'ТОО БЕЗ КП ТЕСТ', comment: 'отказались' },
|
||||||
|
{ name: 'ТОО Asia Techno', comment: 'не работают с маленьким объемом' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(plan.toRemove).toEqual([1]);
|
||||||
|
expect(plan.toCreate).toEqual([{ name: 'ТОО БЕЗ КП ТЕСТ', comment: 'отказались' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('пробелы по краям названия и комментария не считаются изменением', () => {
|
||||||
|
const plan = planNoOfferSync(existing, [
|
||||||
|
{ name: ' ТОО БЕЗ КП ТЕСТ ', comment: 'НЕ ТРОГАТЬ, ТЕСТ!!!!! ' },
|
||||||
|
{ name: 'ТОО Asia Techno', comment: 'не работают с маленьким объемом' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(plan).toEqual({ toCreate: [], toRemove: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNoOfferSuppliers', () => {
|
||||||
|
it('поставщики без КП для списка под таблицей сравнения', () => {
|
||||||
|
const suppliers: Supplier[] = [
|
||||||
|
{ id: '263_277', spId: 263, name: 'ТОО «Арлан Офис Снаб»', hasOffer: true },
|
||||||
|
{ id: 'noOffer_1', name: 'ТОО БЕЗ КП ТЕСТ', hasOffer: false, comment: 'НЕ ТРОГАТЬ' },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getNoOfferSuppliers(suppliers)).toEqual([{ name: 'ТОО БЕЗ КП ТЕСТ', comment: 'НЕ ТРОГАТЬ' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('поставщики без КП при открытии по ссылке', () => {
|
||||||
|
const fromSp = noOfferRecordsToSuppliers([{ id: 28, name: 'ТОО БЕЗ КП ТЕСТ', comment: 'НЕ ТРОГАТЬ' }]);
|
||||||
|
|
||||||
|
it('записи SP становятся поставщиками без КП', () => {
|
||||||
|
expect(fromSp).toEqual([{ id: 'noOffer_28', name: 'ТОО БЕЗ КП ТЕСТ', hasOffer: false, comment: 'НЕ ТРОГАТЬ' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('добавляются к колонкам с КП', () => {
|
||||||
|
const withOffer: Supplier[] = [{ id: '263_277', spId: 263, name: 'Арлан', hasOffer: true }];
|
||||||
|
|
||||||
|
expect(withNoOfferSuppliers(withOffer, fromSp).map(s => s.id)).toEqual(['263_277', 'noOffer_28']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('свои поставщики без КП в сессии не заменяются', () => {
|
||||||
|
const session: Supplier[] = [{ id: 'local_1', name: 'Из сессии', hasOffer: false }];
|
||||||
|
|
||||||
|
expect(withNoOfferSuppliers(session, fromSp)).toEqual(session);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Supplier } from '../types/domain';
|
||||||
|
|
||||||
|
// Поставщик, не предоставивший КП: название и комментарий (почему нет КП).
|
||||||
|
export interface NoOfferSupplierEntry {
|
||||||
|
name: string;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запись списка «Не получены КП» этой СТ в SP.
|
||||||
|
export interface NoOfferSupplierRecord extends NoOfferSupplierEntry {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameEntry = (a: NoOfferSupplierEntry, b: NoOfferSupplierEntry): boolean =>
|
||||||
|
a.name.trim() === b.name.trim() && a.comment.trim() === b.comment.trim();
|
||||||
|
|
||||||
|
// Синхронизация «Не получены КП» одной СТ с тем, что сейчас на шаге 2: новых — создать, удалённых и
|
||||||
|
// изменённых (другой комментарий) — убрать, изменённых — создать заново. Раньше записи только
|
||||||
|
// добавлялись и без привязки к СТ, поэтому по ссылке список было не восстановить.
|
||||||
|
export function planNoOfferSync(
|
||||||
|
existing: NoOfferSupplierRecord[],
|
||||||
|
desired: NoOfferSupplierEntry[],
|
||||||
|
): { toCreate: NoOfferSupplierEntry[]; toRemove: number[] } {
|
||||||
|
return {
|
||||||
|
toCreate: desired.filter(d => !existing.some(e => sameEntry(e, d))),
|
||||||
|
toRemove: existing.filter(e => !desired.some(d => sameEntry(e, d))).map(e => e.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поставщики без КП из колонок формы — для списка под таблицей сравнения (Шаг 3).
|
||||||
|
export function getNoOfferSuppliers(suppliers: Supplier[]): NoOfferSupplierEntry[] {
|
||||||
|
return suppliers
|
||||||
|
.filter(s => s.hasOffer === false)
|
||||||
|
.map(s => ({ name: s.name, comment: s.comment ?? '' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поставщики без КП из SP (открытие по ссылке / без сессии) — в колонки формы с hasOffer: false.
|
||||||
|
// Префикс id — чтобы не пересечься с id колонок поставщиков с КП.
|
||||||
|
export function noOfferRecordsToSuppliers(records: NoOfferSupplierRecord[]): Supplier[] {
|
||||||
|
return records.map(r => ({ id: `noOffer_${r.id}`, name: r.name, hasOffer: false, comment: r.comment }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Колонки формы с поставщиками без КП из SP: сессию не трогаем, если в ней уже есть свои без КП.
|
||||||
|
export function withNoOfferSuppliers(current: Supplier[], fromSp: Supplier[]): Supplier[] {
|
||||||
|
if (fromSp.length === 0 || current.some(s => s.hasOffer === false)) return current;
|
||||||
|
return [...current, ...fromSp];
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ import {
|
|||||||
keepLatestOfferRecords,
|
keepLatestOfferRecords,
|
||||||
parseOfferItemIndex,
|
parseOfferItemIndex,
|
||||||
} from "../../../shared/offers/offerRecords";
|
} from "../../../shared/offers/offerRecords";
|
||||||
|
import {
|
||||||
|
NoOfferSupplierEntry,
|
||||||
|
NoOfferSupplierRecord,
|
||||||
|
planNoOfferSync,
|
||||||
|
} from "../components/utils/noOfferSuppliers";
|
||||||
|
|
||||||
// Round monetary values to 2 decimals before persisting to SharePoint (drops float tails).
|
// Round monetary values to 2 decimals before persisting to SharePoint (drops float tails).
|
||||||
function round2(value: number): number {
|
function round2(value: number): number {
|
||||||
@@ -1288,23 +1293,38 @@ export class CtService {
|
|||||||
return { spId, offerId };
|
return { spId, offerId };
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveNoOfferSupplierItem(name: string, comment?: string): Promise<void> {
|
// «Не получены КП» этой СТ (привязка по лукапу CT). Раньше записи сохранялись без СТ и с дедупом
|
||||||
const escaped = name.replace(/'/g, "''");
|
// по названию на все СТ сразу — по ссылке список поставщиков без КП было не восстановить.
|
||||||
const checkRes = await this.http.get(
|
async loadNoOfferSuppliers(ctId: string): Promise<NoOfferSupplierRecord[]> {
|
||||||
`${this._li(LISTS.NO_OFFERS)}?$filter=Title eq '${escaped}'&$select=Id&$top=1`,
|
const res = await this.http.get(
|
||||||
|
`${this._li(LISTS.NO_OFFERS)}?$select=Id,Title,comment&$filter=CTId eq ${parseInt(ctId, 10)}&$top=500`,
|
||||||
SPHttpClient.configurations.v1,
|
SPHttpClient.configurations.v1,
|
||||||
);
|
);
|
||||||
if (checkRes.ok && ((await checkRes.json()).value ?? []).length > 0) return;
|
if (!res.ok) throw new Error(`Ошибка чтения поставщиков без КП: ${res.status}`);
|
||||||
const res = await this.http.post(
|
const data = await res.json();
|
||||||
this._li(LISTS.NO_OFFERS),
|
return ((data.value ?? []) as Array<Record<string, unknown>>).map((item) => ({
|
||||||
SPHttpClient.configurations.v1,
|
id: Number(item.Id),
|
||||||
{
|
name: String(item.Title ?? ""),
|
||||||
|
comment: String(item.comment ?? ""),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Приводит «Не получены КП» этой СТ к списку с шага 2: новых — создать, удалённых и изменённых — убрать.
|
||||||
|
async syncNoOfferSuppliers(ctId: string, entries: NoOfferSupplierEntry[]): Promise<void> {
|
||||||
|
const existing = await this.loadNoOfferSuppliers(ctId);
|
||||||
|
const { toCreate, toRemove } = planNoOfferSync(existing, entries);
|
||||||
|
await this._runChunked(toRemove, 20, async (id) => {
|
||||||
|
await this.http
|
||||||
|
.post(`${this._liById(LISTS.NO_OFFERS, id)}/recycle()`, SPHttpClient.configurations.v1, {})
|
||||||
|
.catch(() => undefined /* уже в корзине / не найден — игнорируем */);
|
||||||
|
});
|
||||||
|
for (const entry of toCreate) {
|
||||||
|
const res = await this.http.post(this._li(LISTS.NO_OFFERS), SPHttpClient.configurations.v1, {
|
||||||
headers: { "Content-Type": "application/json;odata=nometadata" },
|
headers: { "Content-Type": "application/json;odata=nometadata" },
|
||||||
body: JSON.stringify({ Title: name, comment: comment ?? "" }),
|
body: JSON.stringify({ Title: entry.name.trim(), comment: entry.comment.trim(), CTId: parseInt(ctId, 10) }),
|
||||||
},
|
});
|
||||||
);
|
if (!res.ok) throw new Error(`Ошибка сохранения поставщика без КП: ${res.status}`);
|
||||||
if (!res.ok)
|
}
|
||||||
throw new Error(`Ошибка сохранения поставщика без КП: ${res.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentUserProfile(): Promise<{
|
async getCurrentUserProfile(): Promise<{
|
||||||
|
|||||||
Reference in New Issue
Block a user