fix(ct): шаг 1 видит позиции СТ, открытой из реестра
Позиции СТ из SP не были связаны с потребностями (id = SP Id), поэтому на шаге 1 при «Выбрано: N» ни одна строка не была отмечена, «Только выбранные» показывал пустой список, а снять выбор было негде. - в «Позиции СТ» пишется requestPositionId (GUID потребности__номер), шаг 1 сопоставляет выбор по нему; id позиции остаётся SP Id, к нему привязаны предложения; - потребности, чьи позиции уже в СТ, показываются и вне выбранного периода (позиции догружаются сразу); - проект восстанавливается из шапки СТ — «Сохранить и продолжить» молча ничего не делала и снимался запрет на чужой проект. У СТ, созданных до колонки, связи нет — их позиции в таблице не видны.
This commit is contained in:
+25
-8
@@ -6,7 +6,14 @@ import { CtService } from '../../services/CtService';
|
|||||||
import { CTFormData } from '../hooks/useCtForm';
|
import { CTFormData } from '../hooks/useCtForm';
|
||||||
import { useRequests } from '../hooks/useRequests';
|
import { useRequests } from '../hooks/useRequests';
|
||||||
import { RequestsTable } from './RequestsTable';
|
import { RequestsTable } from './RequestsTable';
|
||||||
import { countSelectedInRequest, planStep1Save, resolveSelectedPositions } from './step1Plan';
|
import {
|
||||||
|
buildPinnedRequests,
|
||||||
|
countSelectedInRequest,
|
||||||
|
planStep1Save,
|
||||||
|
resolveProject,
|
||||||
|
resolveSelectedPositions,
|
||||||
|
selectionKey,
|
||||||
|
} from './step1Plan';
|
||||||
import styles from './Step1PositionSelection.module.scss';
|
import styles from './Step1PositionSelection.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -35,13 +42,21 @@ export const Step1PositionSelection: React.FC<Props> = ({
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [showOnlySelected, setShowOnlySelected] = useState(false);
|
const [showOnlySelected, setShowOnlySelected] = useState(false);
|
||||||
const [selectedPositionIds, setSelectedPositionIds] = useState<Set<string>>(
|
const [selectedPositionIds, setSelectedPositionIds] = useState<Set<string>>(
|
||||||
new Set(formData.selectedPositions.map(p => p.id))
|
new Set(formData.selectedPositions.map(selectionKey))
|
||||||
);
|
);
|
||||||
const [positionMap, setPositionMap] = useState<Map<string, { position: Position; request: Request }>>(new Map());
|
const [positionMap, setPositionMap] = useState<Map<string, { position: Position; request: Request }>>(new Map());
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | undefined>(undefined);
|
const [saveError, setSaveError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const { requests, isLoading, error: loadError, reload, loadPositions, loadingPositionIds } = useRequests(service, days, isReadOnly);
|
// Сохранённые позиции есть только у созданной СТ, поэтому проект для закреплённых потребностей — из её шапки.
|
||||||
|
const ctProjectName = formData.ct?.projectName;
|
||||||
|
const pinnedRequests = useMemo(
|
||||||
|
() => buildPinnedRequests(formData.selectedPositions, resolveProject(undefined, ctProjectName, true)),
|
||||||
|
[formData.selectedPositions, ctProjectName],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { requests, isLoading, error: loadError, reload, loadPositions, loadingPositionIds } =
|
||||||
|
useRequests(service, days, isReadOnly, pinnedRequests);
|
||||||
|
|
||||||
const readOnlyRequests = useMemo(() => {
|
const readOnlyRequests = useMemo(() => {
|
||||||
if (!isReadOnly) return [];
|
if (!isReadOnly) return [];
|
||||||
@@ -93,7 +108,8 @@ export const Step1PositionSelection: React.FC<Props> = ({
|
|||||||
setPositionMap(map);
|
setPositionMap(map);
|
||||||
}, [requests]);
|
}, [requests]);
|
||||||
|
|
||||||
const lockedProjectId = formData.selectedProject?.id;
|
const project = resolveProject(formData.selectedProject, ctProjectName, selectedPositionIds.size > 0);
|
||||||
|
const lockedProjectId = project?.id;
|
||||||
|
|
||||||
const handleSelectionChange = useCallback(
|
const handleSelectionChange = useCallback(
|
||||||
(positionId: string, _position: Position, request: Request, checked: boolean) => {
|
(positionId: string, _position: Position, request: Request, checked: boolean) => {
|
||||||
@@ -166,7 +182,7 @@ export const Step1PositionSelection: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedProject = selectedPositions.length > 0
|
const selectedProject = selectedPositions.length > 0
|
||||||
? positionMap.get(selectedPositions[0].id)?.request.project ?? formData.selectedProject ?? null
|
? positionMap.get(selectedPositions[0].id)?.request.project ?? project ?? null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const canSave = selectedPositions.length > 0;
|
const canSave = selectedPositions.length > 0;
|
||||||
@@ -197,7 +213,8 @@ export const Step1PositionSelection: React.FC<Props> = ({
|
|||||||
if (ct.projectName !== selectedProject.name) {
|
if (ct.projectName !== selectedProject.name) {
|
||||||
await service.updateCTProject(ct.id, selectedProject.name);
|
await service.updateCTProject(ct.id, selectedProject.name);
|
||||||
}
|
}
|
||||||
const removedIds = new Set(plan.toRemove.map(p => p.id));
|
// Предложения привязаны к позиции либо по id из сессии, либо по SP Id (загружены из SP).
|
||||||
|
const removedIds = new Set(plan.toRemove.flatMap(p => [p.id, String(p.spId)]));
|
||||||
onSave({
|
onSave({
|
||||||
ct: { ...ct, projectName: selectedProject.name },
|
ct: { ...ct, projectName: selectedProject.name },
|
||||||
selectedProject,
|
selectedProject,
|
||||||
@@ -250,9 +267,9 @@ export const Step1PositionSelection: React.FC<Props> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.selectedProject && (
|
{project && (
|
||||||
<Alert variant="info">
|
<Alert variant="info">
|
||||||
Выбран проект: <strong>{formData.selectedProject.name}</strong> — позиции из других проектов недоступны
|
Выбран проект: <strong>{project.name}</strong> — позиции из других проектов недоступны
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+92
-1
@@ -1,4 +1,11 @@
|
|||||||
import { countSelectedInRequest, planStep1Save, resolveSelectedPositions } from './step1Plan';
|
import {
|
||||||
|
buildPinnedRequests,
|
||||||
|
countSelectedInRequest,
|
||||||
|
planStep1Save,
|
||||||
|
resolveProject,
|
||||||
|
resolveSelectedPositions,
|
||||||
|
selectionKey,
|
||||||
|
} from './step1Plan';
|
||||||
import { Position } from '../types/domain';
|
import { Position } from '../types/domain';
|
||||||
|
|
||||||
function makePosition(id: string, overrides: Partial<Position> = {}): Position {
|
function makePosition(id: string, overrides: Partial<Position> = {}): Position {
|
||||||
@@ -83,3 +90,87 @@ describe('resolveSelectedPositions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('СТ открыта из реестра — позиции из SP связаны с потребностью через requestPositionId', () => {
|
||||||
|
// СТ №8: позиции пришли из SP (id = SP Id). Таблица шага 1 их не находила — «Выбрано: 3»,
|
||||||
|
// а увидеть и снять выбор было негде.
|
||||||
|
const fromSp = [
|
||||||
|
makePosition('1543', { spId: 1543, requestPositionId: 'req1__0', requestId: 'req1' }),
|
||||||
|
makePosition('1544', { spId: 1544, requestPositionId: 'req1__2', requestId: 'req1' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
it('ключ выбора — позиция потребности, а не SP Id', () => {
|
||||||
|
expect(fromSp.map(selectionKey)).toEqual(['req1__0', 'req1__2']);
|
||||||
|
expect(countSelectedInRequest(new Set(fromSp.map(selectionKey)), 'req1')).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('старая СТ без связи — ключ остаётся SP Id', () => {
|
||||||
|
expect(selectionKey(makePosition('1600', { spId: 1600, requestId: '' }))).toBe('1600');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('повторное сохранение: та же позиция из таблицы не создаётся заново, снятая — удаляется', () => {
|
||||||
|
const plan = planStep1Save(fromSp, [makePosition('req1__0'), makePosition('req1__5')]);
|
||||||
|
|
||||||
|
expect(plan.toKeep.map(p => p.spId)).toEqual([1543]);
|
||||||
|
expect(plan.toCreate.map(p => p.id)).toEqual(['req1__5']);
|
||||||
|
expect(plan.toRemove.map(p => p.spId)).toEqual([1544]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('нераскрытая потребность — выбранная позиция берётся из сохранённых по ключу', () => {
|
||||||
|
const result = resolveSelectedPositions(['req1__2'], () => undefined, fromSp);
|
||||||
|
|
||||||
|
expect(result.map(p => p.spId)).toEqual([1544]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildPinnedRequests', () => {
|
||||||
|
// Потребность старше выбранного периода не приходит в списке — закрепляем её, раз её позиции в СТ.
|
||||||
|
const project = { id: 'Проект X', name: 'Проект X', code: '' };
|
||||||
|
|
||||||
|
it('по одной потребности на каждую, из которой есть позиции в СТ', () => {
|
||||||
|
const saved = [
|
||||||
|
makePosition('1', { requestId: 'req1', requestNumber: 'A' }),
|
||||||
|
makePosition('2', { requestId: 'req2', requestNumber: 'B' }),
|
||||||
|
makePosition('3', { requestId: 'req1', requestNumber: 'A' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(buildPinnedRequests(saved, project).map(r => [r.id, r.requestNumber])).toEqual([
|
||||||
|
['req1', 'A'],
|
||||||
|
['req2', 'B'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('позиции без связи с потребностью (старые СТ) — пропускаем', () => {
|
||||||
|
expect(buildPinnedRequests([makePosition('1600', { requestId: '' })], project)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('проект неизвестен — не закрепляем', () => {
|
||||||
|
expect(buildPinnedRequests([makePosition('req1__0')], undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveProject', () => {
|
||||||
|
// Из реестра selectedProject не восстанавливался → «Сохранить и продолжить» молча ничего не делала
|
||||||
|
// и пропадал запрет выбирать позиции чужого проекта.
|
||||||
|
const project = { id: 'Проект X', name: 'Проект X', code: '' };
|
||||||
|
|
||||||
|
it('берёт выбранный проект, если он есть', () => {
|
||||||
|
expect(resolveProject(project, 'Другой', true)).toBe(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('без выбранного проекта, но с выбором — проект из шапки СТ (id проекта = его название)', () => {
|
||||||
|
expect(resolveProject(undefined, 'Выпуск геомембран', true)).toEqual({
|
||||||
|
id: 'Выпуск геомембран',
|
||||||
|
name: 'Выпуск геомембран',
|
||||||
|
code: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('выбор снят полностью — проект не навязываем, можно выбрать другой', () => {
|
||||||
|
expect(resolveProject(undefined, 'Выпуск геомембран', false)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('СТ ещё не создана — проекта нет', () => {
|
||||||
|
expect(resolveProject(undefined, undefined, true)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+54
-10
@@ -1,8 +1,15 @@
|
|||||||
import { Position } from '../types/domain';
|
import { Position, Project, Request } from '../types/domain';
|
||||||
|
|
||||||
// id позиции заявки — `${id потребности}__${номер}` (см. CtService._mapPosition).
|
// id позиции заявки — `${id потребности}__${номер}` (см. CtService._mapPosition).
|
||||||
const POSITION_ID_SEPARATOR = '__';
|
const POSITION_ID_SEPARATOR = '__';
|
||||||
|
|
||||||
|
// Ключ выбора на шаге 1 — id позиции потребности. У позиции, загруженной из SP (СТ открыта из реестра),
|
||||||
|
// id — это SP Id, а связь с потребностью лежит в requestPositionId. У СТ, созданных до появления
|
||||||
|
// этого поля, связи нет — тогда ключ SP Id, и в таблице потребностей такая позиция не найдётся.
|
||||||
|
export function selectionKey(position: Position): string {
|
||||||
|
return position.requestPositionId || position.id;
|
||||||
|
}
|
||||||
|
|
||||||
// Повторное сохранение шага 1, когда СТ уже создана: что оставить (позиции уже в SP), что дописать,
|
// Повторное сохранение шага 1, когда СТ уже создана: что оставить (позиции уже в SP), что дописать,
|
||||||
// что убрать. Раньше шаг 1 всегда создавал новую СТ — предложения оставались в прежней, а новая
|
// что убрать. Раньше шаг 1 всегда создавал новую СТ — предложения оставались в прежней, а новая
|
||||||
// открывалась по ссылке пустой.
|
// открывалась по ссылке пустой.
|
||||||
@@ -13,13 +20,13 @@ export interface Step1SavePlan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function planStep1Save(saved: Position[], selected: Position[]): Step1SavePlan {
|
export function planStep1Save(saved: Position[], selected: Position[]): Step1SavePlan {
|
||||||
const selectedIds = new Set(selected.map(p => p.id));
|
const selectedKeys = new Set(selected.map(selectionKey));
|
||||||
const savedById = new Map(saved.map(p => [p.id, p]));
|
const savedByKey = new Map(saved.map(p => [selectionKey(p), p]));
|
||||||
|
|
||||||
const toKeep: Position[] = [];
|
const toKeep: Position[] = [];
|
||||||
const toCreate: Position[] = [];
|
const toCreate: Position[] = [];
|
||||||
selected.forEach(p => {
|
selected.forEach(p => {
|
||||||
const savedPosition = savedById.get(p.id);
|
const savedPosition = savedByKey.get(selectionKey(p));
|
||||||
if (savedPosition?.spId !== undefined) {
|
if (savedPosition?.spId !== undefined) {
|
||||||
toKeep.push(savedPosition);
|
toKeep.push(savedPosition);
|
||||||
} else {
|
} else {
|
||||||
@@ -30,7 +37,7 @@ export function planStep1Save(saved: Position[], selected: Position[]): Step1Sav
|
|||||||
return {
|
return {
|
||||||
toKeep,
|
toKeep,
|
||||||
toCreate,
|
toCreate,
|
||||||
toRemove: saved.filter(p => p.spId !== undefined && !selectedIds.has(p.id)),
|
toRemove: saved.filter(p => p.spId !== undefined && !selectedKeys.has(selectionKey(p))),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +52,49 @@ export function countSelectedInRequest(selectedIds: Set<string>, requestId: stri
|
|||||||
// Выбранные позиции: загруженная версия, а если потребность не раскрыта — сохранённая в форме.
|
// Выбранные позиции: загруженная версия, а если потребность не раскрыта — сохранённая в форме.
|
||||||
// Без этого после возврата на шаг 1 выбор есть, а «Сохранить» неактивна.
|
// Без этого после возврата на шаг 1 выбор есть, а «Сохранить» неактивна.
|
||||||
export function resolveSelectedPositions(
|
export function resolveSelectedPositions(
|
||||||
selectedIds: string[],
|
selectedKeys: string[],
|
||||||
findLoaded: (id: string) => Position | undefined,
|
findLoaded: (key: string) => Position | undefined,
|
||||||
saved: Position[],
|
saved: Position[],
|
||||||
): Position[] {
|
): Position[] {
|
||||||
const savedById = new Map(saved.map(p => [p.id, p]));
|
const savedByKey = new Map(saved.map(p => [selectionKey(p), p]));
|
||||||
return selectedIds
|
return selectedKeys
|
||||||
.map(id => findLoaded(id) ?? savedById.get(id))
|
.map(key => findLoaded(key) ?? savedByKey.get(key))
|
||||||
.filter((p): p is Position => p !== undefined);
|
.filter((p): p is Position => p !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Потребности, позиции которых уже в СТ. Их надо показать в таблице, даже если потребность старше
|
||||||
|
// выбранного периода, — иначе «Выбрано: 3», а увидеть и снять выбор негде. Шапку собираем из позиций
|
||||||
|
// (организацию и дату создания в позициях СТ не храним), сами позиции потребности догружает useRequests.
|
||||||
|
export function buildPinnedRequests(saved: Position[], project: Project | undefined): Request[] {
|
||||||
|
if (!project) return [];
|
||||||
|
const byRequestId = new Map<string, Request>();
|
||||||
|
saved.forEach(p => {
|
||||||
|
if (!p.requestId || byRequestId.has(p.requestId)) return;
|
||||||
|
byRequestId.set(p.requestId, {
|
||||||
|
id: p.requestId,
|
||||||
|
requestNumber: p.requestNumber,
|
||||||
|
project,
|
||||||
|
discipline: p.disciplineCode,
|
||||||
|
workType: p.smrType,
|
||||||
|
organization: p.organization ?? '',
|
||||||
|
createdAt: p.requestCreatedAt ?? '',
|
||||||
|
positionsCount: 0,
|
||||||
|
positions: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(byRequestId.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проект СТ. Открыли СТ из реестра — selectedProject в форме нет, берём из шапки СТ (у потребностей
|
||||||
|
// id проекта = его название, см. CtService._mapRequest). Без этого «Сохранить и продолжить» молча
|
||||||
|
// ничего не делала и снимался запрет на позиции чужого проекта. Если выбор снят целиком — проект не
|
||||||
|
// навязываем, чтобы можно было собрать СТ по другому проекту.
|
||||||
|
export function resolveProject(
|
||||||
|
selectedProject: Project | undefined,
|
||||||
|
ctProjectName: string | undefined,
|
||||||
|
hasSelection: boolean,
|
||||||
|
): Project | undefined {
|
||||||
|
if (selectedProject) return selectedProject;
|
||||||
|
if (!hasSelection || !ctProjectName) return undefined;
|
||||||
|
return { id: ctProjectName, name: ctProjectName, code: '' };
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,25 @@ interface UseRequestsResult {
|
|||||||
loadingPositionIds: Set<string>;
|
loadingPositionIds: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRequests(service: CtService, days: number, skip?: boolean): UseRequestsResult {
|
// Потребности, позиции которых уже в СТ, но которые не пришли за выбранный период: показываем
|
||||||
|
// их в таблице и сразу грузим позиции — без них неизвестно реальное число позиций потребности.
|
||||||
|
async function loadMissingPinned(service: CtService, pinned: Request[], loaded: Request[]): Promise<Request[]> {
|
||||||
|
const loadedIds = new Set(loaded.map(r => r.id));
|
||||||
|
const missing = pinned.filter(r => !loadedIds.has(r.id));
|
||||||
|
return Promise.all(
|
||||||
|
missing.map(async request => {
|
||||||
|
const positions = await service.loadRequestPositions(request);
|
||||||
|
return { ...request, positions, positionsCount: positions.length };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRequests(
|
||||||
|
service: CtService,
|
||||||
|
days: number,
|
||||||
|
skip: boolean,
|
||||||
|
pinnedRequests: Request[],
|
||||||
|
): UseRequestsResult {
|
||||||
const [requests, setRequests] = useState<Request[]>([]);
|
const [requests, setRequests] = useState<Request[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(!skip);
|
const [isLoading, setIsLoading] = useState(!skip);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
@@ -34,9 +52,10 @@ export function useRequests(service: CtService, days: number, skip?: boolean): U
|
|||||||
|
|
||||||
service
|
service
|
||||||
.fetchRequests(days)
|
.fetchRequests(days)
|
||||||
.then(data => {
|
.then(async data => {
|
||||||
|
const pinned = await loadMissingPinned(service, pinnedRequests, data);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setRequests(data);
|
setRequests([...data, ...pinned]);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -50,7 +69,7 @@ export function useRequests(service: CtService, days: number, skip?: boolean): U
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [service, days, reloadToken, skip]);
|
}, [service, days, reloadToken, skip, pinnedRequests]);
|
||||||
|
|
||||||
const loadPositions = useCallback(
|
const loadPositions = useCallback(
|
||||||
async (requestId: string): Promise<Position[]> => {
|
async (requestId: string): Promise<Position[]> => {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export interface Project {
|
|||||||
export interface Position {
|
export interface Position {
|
||||||
id: string;
|
id: string;
|
||||||
spId?: number; // SP item ID in Позиции СТ (populated after createCTPositions)
|
spId?: number; // SP item ID in Позиции СТ (populated after createCTPositions)
|
||||||
|
// Позиция потребности, из которой взята позиция СТ: `${GUID потребности}__${номер}` — id позиции
|
||||||
|
// в таблице шага 1. Хранится в SP-поле 'requestPositionId'; у СТ, созданных до его появления, пусто.
|
||||||
|
requestPositionId?: string;
|
||||||
requestId: string; // GUID потребности
|
requestId: string; // GUID потребности
|
||||||
requestNumber: string; // demandNumber — номер потребности (e.g. "081/26-04/0017")
|
requestNumber: string; // demandNumber — номер потребности (e.g. "081/26-04/0017")
|
||||||
materialName: string; // position — наименование позиции
|
materialName: string; // position — наименование позиции
|
||||||
|
|||||||
@@ -342,9 +342,14 @@ export class CtService {
|
|||||||
if (positions.length === 0) return [];
|
if (positions.length === 0) return [];
|
||||||
// Раньше — по одному POST на позицию в цикле await (последовательно): 100 позиций =
|
// Раньше — по одному POST на позицию в цикле await (последовательно): 100 позиций =
|
||||||
// 100 запросов и ~30-60 сек + троттлинг 429. Теперь одним $batch (до 100 операций/запрос).
|
// 100 запросов и ~30-60 сек + троттлинг 429. Теперь одним $batch (до 100 операций/запрос).
|
||||||
|
// Связь с позицией потребности нужна шагу 1, когда СТ открыта из реестра (см. Position.requestPositionId).
|
||||||
|
// У доп. позиций шага 3 потребности нет — requestId пустой, связь не пишем.
|
||||||
|
const requestPositionIdOf = (pos: Position): string =>
|
||||||
|
pos.requestPositionId || (pos.requestId ? pos.id : "");
|
||||||
const bodies = positions.map((pos) => ({
|
const bodies = positions.map((pos) => ({
|
||||||
Title: pos.materialName,
|
Title: pos.materialName,
|
||||||
CTId: parseInt(ctId, 10),
|
CTId: parseInt(ctId, 10),
|
||||||
|
requestPositionId: requestPositionIdOf(pos),
|
||||||
demandNumber: pos.requestNumber,
|
demandNumber: pos.requestNumber,
|
||||||
position: pos.materialName,
|
position: pos.materialName,
|
||||||
disciplineCode: pos.disciplineCode ?? "",
|
disciplineCode: pos.disciplineCode ?? "",
|
||||||
@@ -357,7 +362,11 @@ export class CtService {
|
|||||||
deadline: pos.deadline ? pos.deadline.replace(/Z?$/, "Z") : undefined,
|
deadline: pos.deadline ? pos.deadline.replace(/Z?$/, "Z") : undefined,
|
||||||
}));
|
}));
|
||||||
const ids = await this.batchCreateItems(LISTS.CT_POSITIONS, bodies);
|
const ids = await this.batchCreateItems(LISTS.CT_POSITIONS, bodies);
|
||||||
return positions.map((pos, i) => ({ ...pos, spId: ids[i] }));
|
return positions.map((pos, i) => ({
|
||||||
|
...pos,
|
||||||
|
spId: ids[i],
|
||||||
|
requestPositionId: requestPositionIdOf(pos) || undefined,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пакетное создание элементов списка через SharePoint OData $batch: вместо N POST —
|
// Пакетное создание элементов списка через SharePoint OData $batch: вместо N POST —
|
||||||
@@ -798,17 +807,21 @@ export class CtService {
|
|||||||
async loadPositionsForCT(ctId: string): Promise<Position[]> {
|
async loadPositionsForCT(ctId: string): Promise<Position[]> {
|
||||||
const url =
|
const url =
|
||||||
`${this._li(LISTS.CT_POSITIONS)}` +
|
`${this._li(LISTS.CT_POSITIONS)}` +
|
||||||
`?$select=Id,Title,unit,quantity,positionType,demandNumber,disciplineCode,SMR,agskCode,uniqueCode,deadline` +
|
`?$select=Id,Title,unit,quantity,positionType,requestPositionId,demandNumber,disciplineCode,SMR,agskCode,uniqueCode,deadline` +
|
||||||
`&$filter=CTId eq ${parseInt(ctId, 10)}` +
|
`&$filter=CTId eq ${parseInt(ctId, 10)}` +
|
||||||
`&$top=500`;
|
`&$top=500`;
|
||||||
const res = await this.http.get(url, SPHttpClient.configurations.v1);
|
const res = await this.http.get(url, SPHttpClient.configurations.v1);
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return (data.value ?? []).map(
|
return (data.value ?? []).map((item: Record<string, unknown>): Position => {
|
||||||
(item: Record<string, unknown>): Position => ({
|
// `${GUID потребности}__${номер}`; у СТ, созданных до появления поля, пусто.
|
||||||
|
const requestPositionId = String(item.requestPositionId ?? "");
|
||||||
|
return {
|
||||||
|
// id остаётся SP Id: по нему к позиции привязаны предложения из SP (см. utils/spOffers).
|
||||||
id: String(item["Id"]),
|
id: String(item["Id"]),
|
||||||
spId: Number(item["Id"]),
|
spId: Number(item["Id"]),
|
||||||
requestId: "",
|
requestPositionId: requestPositionId || undefined,
|
||||||
|
requestId: requestPositionId.split("__")[0],
|
||||||
requestNumber: String(item["demandNumber"] ?? ""),
|
requestNumber: String(item["demandNumber"] ?? ""),
|
||||||
materialName: String(item["Title"] ?? ""),
|
materialName: String(item["Title"] ?? ""),
|
||||||
disciplineCode: String(item["disciplineCode"] ?? ""),
|
disciplineCode: String(item["disciplineCode"] ?? ""),
|
||||||
@@ -819,8 +832,8 @@ export class CtService {
|
|||||||
quantity: Number(item["quantity"] ?? 0),
|
quantity: Number(item["quantity"] ?? 0),
|
||||||
positionType: String(item["positionType"] ?? "ТМЦ"),
|
positionType: String(item["positionType"] ?? "ТМЦ"),
|
||||||
deadline: String(item["deadline"] ?? ""),
|
deadline: String(item["deadline"] ?? ""),
|
||||||
}),
|
};
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadFullOffersForPositions(positionSpIds: number[]): Promise<
|
async loadFullOffersForPositions(positionSpIds: number[]): Promise<
|
||||||
|
|||||||
Reference in New Issue
Block a user