Compare commits
1 Commits
f2c601bf99
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e34d290865 |
Generated
+5
-4
@@ -17,7 +17,8 @@
|
|||||||
"@microsoft/sp-webpart-base": "1.22.1",
|
"@microsoft/sp-webpart-base": "1.22.1",
|
||||||
"@pnp/sp": "^4.20.0",
|
"@pnp/sp": "^4.20.0",
|
||||||
"@pnp/spfx-controls-react": "^3.25.0",
|
"@pnp/spfx-controls-react": "^3.25.0",
|
||||||
"@tnsu/monitoring-web": "^0.3.1",
|
"@pnp/telemetry-js": "2.0.0",
|
||||||
|
"@tnsu/monitoring-web": "^0.3.4",
|
||||||
"@tnsu/ui-kit": "^0.15.0",
|
"@tnsu/ui-kit": "^0.15.0",
|
||||||
"jszip": "~3.8.0",
|
"jszip": "~3.8.0",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
@@ -9909,9 +9910,9 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@tnsu/monitoring-web": {
|
"node_modules/@tnsu/monitoring-web": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.4",
|
||||||
"resolved": "https://git.tnsu.kz/api/packages/tansu/npm/%40tnsu%2Fmonitoring-web/-/0.3.1/monitoring-web-0.3.1.tgz",
|
"resolved": "https://git.tnsu.kz/api/packages/tansu/npm/%40tnsu%2Fmonitoring-web/-/0.3.4/monitoring-web-0.3.4.tgz",
|
||||||
"integrity": "sha512-HcHP8Utb9gXBRt4nEC4RW5UrqT8ssIs7Prs2LSQaE/oy3ybFNiqpLp3XbmGx1ssCArZcwBozov0MbIHpC9dVgg==",
|
"integrity": "sha512-tQe0ZM1tBVO3XtbNFliRaaooqvCX3gb/Q/j2lV6+rYCXprodBZByQ6PLrCu/P0me/rW8KdQ02hu7avyLXXfkoQ==",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rrweb/rrweb-plugin-console-record": "^2.2.0",
|
"@rrweb/rrweb-plugin-console-record": "^2.2.0",
|
||||||
|
|||||||
+2
-1
@@ -21,7 +21,8 @@
|
|||||||
"@microsoft/sp-webpart-base": "1.22.1",
|
"@microsoft/sp-webpart-base": "1.22.1",
|
||||||
"@pnp/sp": "^4.20.0",
|
"@pnp/sp": "^4.20.0",
|
||||||
"@pnp/spfx-controls-react": "^3.25.0",
|
"@pnp/spfx-controls-react": "^3.25.0",
|
||||||
"@tnsu/monitoring-web": "^0.3.1",
|
"@pnp/telemetry-js": "2.0.0",
|
||||||
|
"@tnsu/monitoring-web": "^0.3.4",
|
||||||
"@tnsu/ui-kit": "^0.15.0",
|
"@tnsu/ui-kit": "^0.15.0",
|
||||||
"jszip": "~3.8.0",
|
"jszip": "~3.8.0",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@microsoft/sp-property-pane';
|
} from '@microsoft/sp-property-pane';
|
||||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||||
|
import PnPTelemetry from '@pnp/telemetry-js';
|
||||||
import { initMonitoring, spfxTokenProvider } from '@tnsu/monitoring-web';
|
import { initMonitoring, spfxTokenProvider } from '@tnsu/monitoring-web';
|
||||||
|
|
||||||
import * as strings from 'RequestPortalWebPartStrings';
|
import * as strings from 'RequestPortalWebPartStrings';
|
||||||
@@ -104,6 +105,9 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
// Один на страницу: повторный вызов (несколько веб-частей) библиотека игнорирует сама.
|
// Один на страницу: повторный вызов (несколько веб-частей) библиотека игнорирует сама.
|
||||||
initMonitoring({ service: 'request-portal', getAccessToken: spfxTokenProvider(this.context.aadTokenProviderFactory) });
|
initMonitoring({ service: 'request-portal', getAccessToken: spfxTokenProvider(this.context.aadTokenProviderFactory) });
|
||||||
installErrorLogging(); // до всего остального — чтобы поймать и ранние отклонения
|
installErrorLogging(); // до всего остального — чтобы поймать и ранние отклонения
|
||||||
|
// Телеметрия PnP (её включает @pnp/spfx-controls-react) шлёт fetch на pnptelemetryproxy.azurewebsites.net
|
||||||
|
// без catch: где адрес закрыт прокси или блокировщиком, это необработанный отказ промиса в мониторинге.
|
||||||
|
PnPTelemetry.getInstance().optOut();
|
||||||
initSP(this.context); // единый клиент PnPjs для записи в списки
|
initSP(this.context); // единый клиент PnPjs для записи в списки
|
||||||
|
|
||||||
// Адрес approval-route должен быть выставлен ДО загрузки партиций — они
|
// Адрес approval-route должен быть выставлен ДО загрузки партиций — они
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { log } from '@tnsu/monitoring-web';
|
||||||
import { getSP } from '../pnp/pnpjs';
|
import { getSP } from '../pnp/pnpjs';
|
||||||
import '@pnp/sp/webs';
|
import '@pnp/sp/webs';
|
||||||
import '@pnp/sp/lists';
|
import '@pnp/sp/lists';
|
||||||
@@ -25,6 +26,14 @@ export interface IPendingRow extends IApprovalRow {
|
|||||||
isDelegator: boolean;
|
isDelegator: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Решение → текст события в логах мониторинга.
|
||||||
|
const DECISION_MESSAGE: Record<ApproverDecision, string> = {
|
||||||
|
approve: 'Заявка согласована',
|
||||||
|
return: 'Заявка возвращена на доработку',
|
||||||
|
reject: 'Заявка отклонена',
|
||||||
|
execute: 'Заявка выполнена исполнителем',
|
||||||
|
};
|
||||||
|
|
||||||
// Решение → статус его строки в списке согласования.
|
// Решение → статус его строки в списке согласования.
|
||||||
const ROW_STATUS: Record<ApproverDecision, string> = {
|
const ROW_STATUS: Record<ApproverDecision, string> = {
|
||||||
approve: REQUEST_STATUS.approved,
|
approve: REQUEST_STATUS.approved,
|
||||||
@@ -336,6 +345,7 @@ export class ApproverService {
|
|||||||
const rows = await this.approval.getForRequest(requestId);
|
const rows = await this.approval.getForRequest(requestId);
|
||||||
const requestStatus = this.computeRequestStatus(rows);
|
const requestStatus = this.computeRequestStatus(rows);
|
||||||
await this.updateRequestStatus(requestId, requestStatus);
|
await this.updateRequestStatus(requestId, requestStatus);
|
||||||
|
await this.logDecision(requestId, rowId, decision, comment, requestStatus);
|
||||||
emitRequestsChanged();
|
emitRequestsChanged();
|
||||||
return requestStatus;
|
return requestStatus;
|
||||||
}
|
}
|
||||||
@@ -364,6 +374,42 @@ export class ApproverService {
|
|||||||
return REQUEST_STATUS.execution;
|
return REQUEST_STATUS.execution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Решение — в логи мониторинга, с кодом и названием заявки, чтобы её было видно прямо в списке логов.
|
||||||
|
* Код и название дочитываются отдельно; не получилось — пишем без них: решение уже применено.
|
||||||
|
*/
|
||||||
|
private async logDecision(
|
||||||
|
requestId: number,
|
||||||
|
rowId: number,
|
||||||
|
decision: ApproverDecision,
|
||||||
|
comment: string,
|
||||||
|
requestStatus: string
|
||||||
|
): Promise<void> {
|
||||||
|
let code = '';
|
||||||
|
let title = '';
|
||||||
|
try {
|
||||||
|
const item = (await this.dataList().items.getById(requestId).select('Title', 'RequestUniqueCode')()) as {
|
||||||
|
Title?: string;
|
||||||
|
RequestUniqueCode?: string;
|
||||||
|
};
|
||||||
|
code = item.RequestUniqueCode || '';
|
||||||
|
title = item.Title || '';
|
||||||
|
} catch {
|
||||||
|
// без кода и названия — лог всё равно нужен
|
||||||
|
}
|
||||||
|
const what = [code, title].filter(Boolean).join(' · ') || `№${requestId}`;
|
||||||
|
log.info(`${DECISION_MESSAGE[decision]}: ${what}`, {
|
||||||
|
'request.id': requestId,
|
||||||
|
'request.code': code,
|
||||||
|
'request.title': title,
|
||||||
|
'request.list': this.dataListRef,
|
||||||
|
'request.status': requestStatus,
|
||||||
|
'approval.decision': decision,
|
||||||
|
'approval.rowId': rowId,
|
||||||
|
'approval.comment': comment.slice(0, 500),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private dataList(): IList {
|
private dataList(): IList {
|
||||||
return this.dataListRef.startsWith('/')
|
return this.dataListRef.startsWith('/')
|
||||||
? getSP().web.getList(this.dataListRef)
|
? getSP().web.getList(this.dataListRef)
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import '@pnp/sp/webs';
|
|||||||
import '@pnp/sp/lists';
|
import '@pnp/sp/lists';
|
||||||
import '@pnp/sp/items';
|
import '@pnp/sp/items';
|
||||||
import { IList } from '@pnp/sp/lists';
|
import { IList } from '@pnp/sp/lists';
|
||||||
|
import { log } from '@tnsu/monitoring-web';
|
||||||
import { getSP } from './pnp/pnpjs';
|
import { getSP } from './pnp/pnpjs';
|
||||||
import { buildRequestUniqueCode } from './requestCode';
|
import { buildRequestUniqueCode } from './requestCode';
|
||||||
import { dataListTitle } from './listTitles';
|
import { dataListTitle } from './listTitles';
|
||||||
import { RequestTypeId } from '../models/types';
|
import { RequestTypeId } from '../models/types';
|
||||||
|
import { REQUEST_TYPES } from '../data/requests';
|
||||||
|
import { isSubmittedStatus } from './approval/models/statuses';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Список данных, в который фактически пишем: listId (конкретная партиция —
|
* Список данных, в который фактически пишем: listId (конкретная партиция —
|
||||||
@@ -50,6 +53,7 @@ export async function saveRequestItem(
|
|||||||
|
|
||||||
if (existingId) {
|
if (existingId) {
|
||||||
await list.items.getById(existingId).update(item);
|
await list.items.getById(existingId).update(item);
|
||||||
|
logSaved(formId, existingId, undefined, item, false);
|
||||||
return { id: existingId };
|
return { id: existingId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,5 +62,34 @@ export async function saveRequestItem(
|
|||||||
// Код известен только после создания элемента (нужен его Id).
|
// Код известен только после создания элемента (нужен его Id).
|
||||||
const code = buildRequestUniqueCode(codePrefix, id, new Date());
|
const code = buildRequestUniqueCode(codePrefix, id, new Date());
|
||||||
await list.items.getById(id).update({ RequestUniqueCode: code });
|
await list.items.getById(id).update({ RequestUniqueCode: code });
|
||||||
|
logSaved(formId, id, code, item, true);
|
||||||
return { id, code };
|
return { id, code };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* События для мониторинга: какая заявка создана и отправлена на согласование (пользователь — из токена).
|
||||||
|
* В тексте — вид, код и название заявки, чтобы её было видно прямо в списке логов. Решения согласующих
|
||||||
|
* пишет approverDecision.service.ts.
|
||||||
|
*/
|
||||||
|
function logSaved(
|
||||||
|
formId: RequestTypeId,
|
||||||
|
id: number,
|
||||||
|
code: string | undefined,
|
||||||
|
item: Record<string, unknown>,
|
||||||
|
created: boolean
|
||||||
|
): void {
|
||||||
|
const typeTitle = REQUEST_TYPES.find((t) => t.id === formId)?.title ?? formId;
|
||||||
|
const title = typeof item.Title === 'string' ? item.Title : '';
|
||||||
|
const status = typeof item.RequestStatus === 'string' ? item.RequestStatus : '';
|
||||||
|
const what = [typeTitle, code, title].filter(Boolean).join(' · ');
|
||||||
|
const attributes = {
|
||||||
|
'request.type': formId,
|
||||||
|
'request.typeTitle': typeTitle,
|
||||||
|
'request.id': id,
|
||||||
|
'request.code': code ?? '',
|
||||||
|
'request.title': title,
|
||||||
|
'request.status': status,
|
||||||
|
};
|
||||||
|
if (created) log.info(`Создана заявка: ${what}`, attributes);
|
||||||
|
if (isSubmittedStatus(status)) log.info(`Заявка отправлена на согласование: ${what}`, attributes);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user