diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx index 071562f..e60d972 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx @@ -47,8 +47,8 @@ import { } from '../../shared/dictionary'; import { ILineItem } from '../../shared/ui'; import { requestLinkUrl } from '../../shared/routing'; -import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service'; import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service'; +import { EXTERNAL_SOURCE } from '../../shared/config'; import { friendlyErrorMessage } from '../../shared/diagnostics'; import { todayIsoDate } from '../../shared/dateUtils'; import { IRequestFormProps } from '../../models/types'; @@ -61,6 +61,7 @@ import { import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { saveTmcTransferRequest, tmcTransferApprovalService } from './submit'; import { loadTmcTransferRequest } from './load'; +import { writeSourceLink } from './writeSourceLink'; import { HeaderSection } from './sections/HeaderSection'; import { ItemsSection } from './sections/ItemsSection'; import { useStableCallback } from '../../shared/useStableCallback'; @@ -195,9 +196,10 @@ const TmcTransferForm: React.FC = ({ // Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK // с проектом (см. shared/dataverse/lzkPosition.service). Необязательное // удобство — на ошибке просто не предзаполняем поле, пользователь выберет - // проект вручную. + // проект вручную. Для ссылок из КПС не ищем: там positionId — позиция акта + // дефектов, а не строка Dataverse (запрос вернул бы только 404). useEffect(() => { - if (editId || !positionId) { + if (editId || !positionId || sourceSystem === EXTERNAL_SOURCE.kps) { return; } let alive = true; @@ -229,7 +231,7 @@ const TmcTransferForm: React.FC = ({ return () => { alive = false; }; - }, [editId, positionId, context]); + }, [editId, positionId, sourceSystem, context]); // Выбор другого заявителя подтягивает его ФИО (для печати) и организации (как // в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь @@ -409,18 +411,15 @@ const TmcTransferForm: React.FC = ({ dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); - // Обратная связь во внешнюю систему: пишем ссылку на эту заявку в - // строку-источник Dataverse (только если заявка открыта по внешней ссылке). + // Обратная связь во внешнюю систему (КПС или Dataverse — см. writeSourceLink): + // отдаём ссылку на эту заявку, если её открыли по внешней ссылке. // Не критично для самой заявки — она уже сохранена и отправлена, поэтому - // сбой записи не роняем, а только логируем (см. флаг ниже для UX-решения). - if (data.SourcePositionId) { - const link = requestLinkUrl(FORM_ID, id, writeListId); - try { - await writeDemandLinePortalLink(context, data.SourcePositionId, link); - } catch (e) { - // eslint-disable-next-line no-console - console.warn('Не удалось записать ссылку в строку-источник (Dataverse):', e); - } + // сбой записи не роняем, а только логируем. + try { + await writeSourceLink(context, data, requestLinkUrl(FORM_ID, id, writeListId)); + } catch (e) { + // eslint-disable-next-line no-console + console.warn(`Не удалось передать ссылку на заявку во внешнюю систему (${data.SourceSystem || 'dataverse'}):`, e); } } }; diff --git a/src/webparts/requestPortal/forms/TmcTransfer/writeSourceLink.ts b/src/webparts/requestPortal/forms/TmcTransfer/writeSourceLink.ts new file mode 100644 index 0000000..4602494 --- /dev/null +++ b/src/webparts/requestPortal/forms/TmcTransfer/writeSourceLink.ts @@ -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 { + if (!data.SourcePositionId) { + return; + } + if (data.SourceSystem === EXTERNAL_SOURCE.kps) { + await writeDefectActPartTmcLink(context, data.SourcePositionId, link); + return; + } + await writeDemandLinePortalLink(context, data.SourcePositionId, link); +} diff --git a/src/webparts/requestPortal/shared/config.ts b/src/webparts/requestPortal/shared/config.ts index 6bda410..d9bf5c9 100644 --- a/src/webparts/requestPortal/shared/config.ts +++ b/src/webparts/requestPortal/shared/config.ts @@ -88,6 +88,24 @@ export const DICTIONARY_API_BASE_URL = 'https://api.tnsu.kz'; */ 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-список на корневом * сайте, где живёт главная страница портала (portal-main-page читает его же). diff --git a/src/webparts/requestPortal/shared/inventory/defectActPart.service.ts b/src/webparts/requestPortal/shared/inventory/defectActPart.service.ts new file mode 100644 index 0000000..f37eac1 --- /dev/null +++ b/src/webparts/requestPortal/shared/inventory/defectActPart.service.ts @@ -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 { + 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}`); + } +}