feat: ссылка на заявку ТМЦ уходит в КПС для ссылок с source=kps
Заявки, открытые из КПС, передают ссылку на заявку в позицию акта
дефектов через Inventory API (POST /inventory/api/defect-acts/parts/
{partId}/tmc-transfer); остальные — в Dataverse, как раньше. Для КПС не
ищем «Куда» в Dataverse: positionId там чужой и давал только 404.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,8 +47,8 @@ import {
|
|||||||
} from '../../shared/dictionary';
|
} from '../../shared/dictionary';
|
||||||
import { ILineItem } from '../../shared/ui';
|
import { ILineItem } from '../../shared/ui';
|
||||||
import { requestLinkUrl } from '../../shared/routing';
|
import { requestLinkUrl } from '../../shared/routing';
|
||||||
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
|
|
||||||
import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service';
|
import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service';
|
||||||
|
import { EXTERNAL_SOURCE } from '../../shared/config';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { todayIsoDate } from '../../shared/dateUtils';
|
import { todayIsoDate } from '../../shared/dateUtils';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
@@ -61,6 +61,7 @@ import {
|
|||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { saveTmcTransferRequest, tmcTransferApprovalService } from './submit';
|
import { saveTmcTransferRequest, tmcTransferApprovalService } from './submit';
|
||||||
import { loadTmcTransferRequest } from './load';
|
import { loadTmcTransferRequest } from './load';
|
||||||
|
import { writeSourceLink } from './writeSourceLink';
|
||||||
import { HeaderSection } from './sections/HeaderSection';
|
import { HeaderSection } from './sections/HeaderSection';
|
||||||
import { ItemsSection } from './sections/ItemsSection';
|
import { ItemsSection } from './sections/ItemsSection';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
@@ -195,9 +196,10 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
||||||
// с проектом (см. shared/dataverse/lzkPosition.service). Необязательное
|
// с проектом (см. shared/dataverse/lzkPosition.service). Необязательное
|
||||||
// удобство — на ошибке просто не предзаполняем поле, пользователь выберет
|
// удобство — на ошибке просто не предзаполняем поле, пользователь выберет
|
||||||
// проект вручную.
|
// проект вручную. Для ссылок из КПС не ищем: там positionId — позиция акта
|
||||||
|
// дефектов, а не строка Dataverse (запрос вернул бы только 404).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editId || !positionId) {
|
if (editId || !positionId || sourceSystem === EXTERNAL_SOURCE.kps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -229,7 +231,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [editId, positionId, context]);
|
}, [editId, positionId, sourceSystem, context]);
|
||||||
|
|
||||||
// Выбор другого заявителя подтягивает его ФИО (для печати) и организации (как
|
// Выбор другого заявителя подтягивает его ФИО (для печати) и организации (как
|
||||||
// в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь
|
// в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь
|
||||||
@@ -409,18 +411,15 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
|
approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
|
||||||
});
|
});
|
||||||
// Обратная связь во внешнюю систему: пишем ссылку на эту заявку в
|
// Обратная связь во внешнюю систему (КПС или Dataverse — см. writeSourceLink):
|
||||||
// строку-источник Dataverse (только если заявка открыта по внешней ссылке).
|
// отдаём ссылку на эту заявку, если её открыли по внешней ссылке.
|
||||||
// Не критично для самой заявки — она уже сохранена и отправлена, поэтому
|
// Не критично для самой заявки — она уже сохранена и отправлена, поэтому
|
||||||
// сбой записи не роняем, а только логируем (см. флаг ниже для UX-решения).
|
// сбой записи не роняем, а только логируем.
|
||||||
if (data.SourcePositionId) {
|
try {
|
||||||
const link = requestLinkUrl(FORM_ID, id, writeListId);
|
await writeSourceLink(context, data, requestLinkUrl(FORM_ID, id, writeListId));
|
||||||
try {
|
} catch (e) {
|
||||||
await writeDemandLinePortalLink(context, data.SourcePositionId, link);
|
// eslint-disable-next-line no-console
|
||||||
} catch (e) {
|
console.warn(`Не удалось передать ссылку на заявку во внешнюю систему (${data.SourceSystem || 'dataverse'}):`, e);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('Не удалось записать ссылку в строку-источник (Dataverse):', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { EXTERNAL_SOURCE } from '../../shared/config';
|
||||||
|
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
|
||||||
|
import { writeDefectActPartTmcLink } from '../../shared/inventory/defectActPart.service';
|
||||||
|
import { IFormData } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответить внешней системе, из которой открыли форму, ссылкой на заявку.
|
||||||
|
* Куда — по SourceSystem (параметр source внешней ссылки):
|
||||||
|
* - 'kps' → Inventory API, позиция акта дефектов (SourcePositionId = partId);
|
||||||
|
* - иначе → Dataverse, «Строка заявки» (SourcePositionId = id строки).
|
||||||
|
* Заявка без SourcePositionId (заведена вручную) — ничего не делаем.
|
||||||
|
*/
|
||||||
|
export async function writeSourceLink(context: WebPartContext, data: IFormData, link: string): Promise<void> {
|
||||||
|
if (!data.SourcePositionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.SourceSystem === EXTERNAL_SOURCE.kps) {
|
||||||
|
await writeDefectActPartTmcLink(context, data.SourcePositionId, link);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await writeDemandLinePortalLink(context, data.SourcePositionId, link);
|
||||||
|
}
|
||||||
@@ -88,6 +88,24 @@ export const DICTIONARY_API_BASE_URL = 'https://api.tnsu.kz';
|
|||||||
*/
|
*/
|
||||||
export const SKUD_EVENTS_PATH = '/Hikvision/Events/CurrentUser';
|
export const SKUD_EVENTS_PATH = '/Hikvision/Events/CurrentUser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значения параметра `source` внешней ссылки (см. shared/routing, sourceSystem) —
|
||||||
|
* из какой системы открыли форму и, значит, куда отвечать ссылкой на заявку.
|
||||||
|
* Без source (или с неизвестным значением) — ссылка пришла из Dataverse.
|
||||||
|
*/
|
||||||
|
export const EXTERNAL_SOURCE = {
|
||||||
|
/** КПС: positionId — id позиции акта дефектов, ответ — в Inventory API. */
|
||||||
|
kps: 'kps'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inventory API (КПС) — приём ссылки на заявку «СЗ на перемещение ТМЦ» для
|
||||||
|
* позиции акта дефектов: POST {DICTIONARY_API_BASE_URL}<путь> с телом { url }.
|
||||||
|
* {partId} = positionId из внешней ссылки с source=kps. Тот же gateway
|
||||||
|
* api.tnsu.kz — авторизация та же, что у справочников (APPROVAL_API_CLIENT_ID).
|
||||||
|
*/
|
||||||
|
export const INVENTORY_TMC_TRANSFER_PATH = '/inventory/api/defect-acts/parts/{partId}/tmc-transfer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Производственный календарь (праздничные выходные) — SP-список на корневом
|
* Производственный календарь (праздничные выходные) — SP-список на корневом
|
||||||
* сайте, где живёт главная страница портала (portal-main-page читает его же).
|
* сайте, где живёт главная страница портала (portal-main-page читает его же).
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { APPROVAL_API_CLIENT_ID, DICTIONARY_API_BASE_URL, INVENTORY_TMC_TRANSFER_PATH } from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обратная связь в КПС: передать ссылку на заявку портала в позицию акта
|
||||||
|
* дефектов, из которой открыли форму «СЗ на перемещение ТМЦ» (source=kps).
|
||||||
|
*
|
||||||
|
* partId — positionId из внешней ссылки (id позиции акта в КПС, НЕ строка
|
||||||
|
* Dataverse). Ответ сервиса ({ partId, defectActId, tmcTransferUrl }) нам не
|
||||||
|
* нужен — достаточно успешного статуса.
|
||||||
|
*/
|
||||||
|
export async function writeDefectActPartTmcLink(
|
||||||
|
context: WebPartContext,
|
||||||
|
partId: string,
|
||||||
|
portalRequestUrl: string
|
||||||
|
): Promise<void> {
|
||||||
|
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||||
|
const path = INVENTORY_TMC_TRANSFER_PATH.replace('{partId}', encodeURIComponent(partId));
|
||||||
|
|
||||||
|
const response: HttpClientResponse = await client.post(`${DICTIONARY_API_BASE_URL}${path}`, AadHttpClient.configurations.v1, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url: portalRequestUrl })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errBody = '';
|
||||||
|
try {
|
||||||
|
errBody = await response.text();
|
||||||
|
} catch {
|
||||||
|
// тело ошибки прочитать не удалось — оставляем пустым
|
||||||
|
}
|
||||||
|
throw new Error(`Inventory POST ${path}: ${response.status} — ${errBody}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user