Files
request-portal/src/webparts/requestPortal/RequestPortalWebPart.ts
T
gorbenko.pavel bed190835e feat: заявление на отпуск уходит на подпись в ЭДО после финального согласования
Когда последний согласующий согласовывает заявку на отпуск, заявление собирается в браузере
как DOCX и отправляется в ЭДО (POST /api/v1/integration/signing-requests). Подписывает сотрудник
из заявки, его же почта передаётся как initiator_email. ИИН берётся из Employee/ByEmail,
БИН компании — из Dictionary/Companies. Неудача отправки не отменяет согласование и
показывается сообщением с кнопкой повтора.

Свойство веб-части «С каким ЭДО работать»: стейдж (https://10.0.1.225:8443) или локальный
бэкенд (https://localhost:7016). jszip объявлен зависимостью явно (раньше был транзитивным).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:46:43 +05:00

269 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
type IPropertyPaneConfiguration,
PropertyPaneDropdown,
PropertyPaneTextField,
PropertyPaneToggle
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import * as strings from 'RequestPortalWebPartStrings';
import RequestPortal from './components/RequestPortal';
import { IRequestPortalProps } from './components/IRequestPortalProps';
import { initSP } from './shared/pnp/pnpjs';
import { installErrorLogging } from './shared/diagnostics';
import { setImpersonatedIdentity } from './shared/impersonation';
import {
APPROVAL_API_BASE_URL_LOCAL,
APPROVAL_API_BASE_URL_PROD,
EDO_API_BASE_URL_LOCAL,
EDO_API_BASE_URL_STAGE,
setApprovalApiBaseUrl,
setEdoApiBaseUrl,
setNotificationSettings
} from './shared/config';
import { initListPartitions } from './shared/approval';
import { ErrorOverlay } from './shared/ui';
import { PropertyPaneDebugUser } from './propertyPane/PropertyPaneDebugUser';
export interface IRequestPortalWebPartProps {
/**
* Отладочная подмена личности — см. shared/impersonation.ts. На бою пусто.
* Одно поле на оба случая: e-mail, введённый напрямую, хранится как есть;
* ФИО, найденное поиском по имени (см. propertyPane/DebugUserField), после
* резолва заменяется собой же на найденный e-mail — отдельное поле под
* «сырой» ввод не нужно, DebugUserField ведёт его в своём локальном состоянии.
*/
debugUserQuery: string;
debugUserDisplayName: string;
/** Куда шлём запросы approval-route (маршруты согласования) — прод или локальный backend. */
approvalApiBaseUrl: string;
/**
* С каким ЭДО работаем при отправке документов на подписание — локальным или
* стейджинговым. У веб-частей, добавленных до появления свойства, оно пустое — это стейдж.
*/
edoApiBaseUrl: string;
/**
* Уведомления согласующим (поток Power Automate). По умолчанию включены:
* у веб-частей, добавленных до появления свойства, оно undefined — это тоже «вкл».
*/
notificationsEnabled?: boolean;
/** Непустой адрес — письма только если активный шаг назначен на него (обкатка). Пусто — всем. */
notifyOnlyTo?: string;
}
export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequestPortalWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
public render(): void {
// Именно здесь, а не в onInit: свойство меняется в панели без перезапуска
// веб-части, и render вызывается на каждое такое изменение.
setImpersonatedIdentity(this.properties.debugUserQuery, this.properties.debugUserDisplayName);
this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD;
setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl);
this.properties.edoApiBaseUrl = this.properties.edoApiBaseUrl || EDO_API_BASE_URL_STAGE;
setEdoApiBaseUrl(this.properties.edoApiBaseUrl);
this._applyNotificationSettings();
const element: React.ReactElement = React.createElement(
ErrorOverlay,
undefined,
React.createElement<IRequestPortalProps>(RequestPortal, {
// key на подменённом e-mail: смена подмены в property pane должна
// пересобрать раздел заново — иначе счётчики в боковом меню и открытые
// таблицы остались бы от прежнего пользователя (их эффекты завязаны на
// монтирование/context, а не на identity подмены).
key: this.properties.debugUserQuery || '__self__',
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName,
context: this.context
})
);
ReactDom.render(element, this.domElement);
}
protected onInit(): Promise<void> {
installErrorLogging(); // до всего остального — чтобы поймать и ранние отклонения
initSP(this.context); // единый клиент PnPjs для записи в списки
// Адрес approval-route должен быть выставлен ДО загрузки партиций — они
// берутся именно оттуда. В render() он ставится снова (свойство меняется в
// панели без перезапуска), но render случается позже onInit.
this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD;
setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl);
this.properties.edoApiBaseUrl = this.properties.edoApiBaseUrl || EDO_API_BASE_URL_STAGE;
setEdoApiBaseUrl(this.properties.edoApiBaseUrl);
this._applyNotificationSettings();
// Партиции грузим здесь, а не по требованию из форм: имена списков нужны
// формам синхронно (см. shared/listTitles.ts), а формы — лениво подгружаемые
// чанки, монтируются заведомо позже onInit. initListPartitions не бросает:
// при недоступном approval-route портал работает по конвенции имён.
//
// НЕ ждём эту загрузку здесь (не Promise.all, а просто вызов) — onInit
// блокирует ПЕРВЫЙ render() всего портала (SPFx не рисует веб-часть, пока
// его промис не разрешится), а два новых AAD-запроса к внешнему api.tnsu.kz
// не обязаны попадать в этот путь: раньше onInit резолвился почти мгновенно
// (только _getEnvironmentMessage, без сети), и весь портал — включая
// «Мои заявки» и «Новая заявка», которым партиции вообще не нужны сразу —
// ждал открытия форм. Партиции при этом гарантированно успевают раньше,
// чем смонтируется первая форма (ленивый чанк грузится дольше).
// eslint-disable-next-line no-void -- без void это «висячий» промис, а no-floating-promises здесь уровня error
void initListPartitions(this.context);
return this._getEnvironmentMessage().then(message => {
this._environmentMessage = message;
});
}
private _applyNotificationSettings(): void {
setNotificationSettings(this.properties.notificationsEnabled !== false, this.properties.notifyOnlyTo || '');
}
private _getEnvironmentMessage(): Promise<string> {
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
.then(context => {
let environmentMessage: string;
switch (context.app.host.name) {
case 'Office': // running in Office
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
break;
case 'Outlook': // running in Outlook
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
break;
case 'Teams': // running in Teams
case 'TeamsModern':
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
break;
default:
environmentMessage = strings.UnknownEnvironment;
}
return environmentMessage;
});
}
return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const {
semanticColors
} = currentTheme;
if (semanticColors) {
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
this.domElement.style.setProperty('--link', semanticColors.link || null);
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
}
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneDebugUser('debugUserQuery', {
label: strings.DebugUserFieldLabel,
description: strings.DebugUserFieldDescription,
context: this.context,
initialQuery: this.properties.debugUserQuery,
onResolved: (query, email, displayName) => {
// Сохраняем ТОЛЬКО e-mail (введённый напрямую или найденный по
// ФИО) — свойство одно и то же используется как e-mail для
// подмены (см. render() выше), так что писать в него сырое ФИО
// нельзя. Пустой query — поле очистили, подмену выключаем.
// А вот «email пуст, но query не пуст» — ФИО ещё не нашлось
// (недописано/не существует): ничего не трогаем, иначе неудачный
// промежуточный поиск молча выключил бы уже работающую подмену.
if (email) {
this.properties.debugUserQuery = email;
this.properties.debugUserDisplayName = displayName;
this.render();
} else if (!query) {
this.properties.debugUserQuery = '';
this.properties.debugUserDisplayName = '';
this.render();
}
}
})
]
},
{
groupName: 'Approval Route API',
groupFields: [
PropertyPaneDropdown('approvalApiBaseUrl', {
label: 'Api Base Url',
selectedKey: this.properties.approvalApiBaseUrl,
options: [
{ key: APPROVAL_API_BASE_URL_PROD, text: `Прод (${APPROVAL_API_BASE_URL_PROD})` },
{ key: APPROVAL_API_BASE_URL_LOCAL, text: `Локально (${APPROVAL_API_BASE_URL_LOCAL})` }
]
})
]
},
{
groupName: 'ЭДО (подписание)',
groupFields: [
PropertyPaneDropdown('edoApiBaseUrl', {
label: 'С каким ЭДО работать',
selectedKey: this.properties.edoApiBaseUrl || EDO_API_BASE_URL_STAGE,
options: [
{ key: EDO_API_BASE_URL_STAGE, text: `Стейдж (${EDO_API_BASE_URL_STAGE})` },
{ key: EDO_API_BASE_URL_LOCAL, text: `Локально (${EDO_API_BASE_URL_LOCAL})` }
]
})
]
},
{
groupName: 'Уведомления',
groupFields: [
PropertyPaneToggle('notificationsEnabled', {
label: 'Уведомления согласующим',
onText: 'Включены',
offText: 'Выключены',
checked: this.properties.notificationsEnabled !== false
}),
PropertyPaneTextField('notifyOnlyTo', {
label: 'Слать только на адрес',
description: 'Для обкатки: письмо уходит, только если шаг назначен на этот e-mail. Пусто — всем.',
placeholder: 'user@tnsu.kz',
disabled: this.properties.notificationsEnabled === false
})
]
}
]
}
]
};
}
}