Sla (#11)
Tests / backend-test (push) Successful in 1m50s
Tests / frontend-test (push) Successful in 49s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/11
This commit was merged in pull request #11.
This commit is contained in:
2026-08-13 04:44:08 +00:00
parent 7f2aabbf29
commit d076f4e98d
103 changed files with 2606 additions and 1265 deletions
@@ -42,7 +42,10 @@ const FIELDS = {
comment: 'Comment',
order: 'StepOrder',
allowReassign: 'AllowReassignToOtherApprovers',
phase: 'Phase'
phase: 'Phase',
// Колонки SLA заводятся провижинингом в request-portal вместе с остальными.
slaHours: 'SLAhours',
askOverdueReason: 'AskOverdueReason'
};
function siteOrigin(siteUrl: string): string {
@@ -152,7 +155,22 @@ export class RequestManageService {
});
const rawRows = (result.PrimarySearchResults ?? []) as Array<{ ListItemID?: string }>;
const ids = rawRows.map(r => Number(r.ListItemID)).filter(id => !isNaN(id) && id > 0);
// Дедуп по Id обязателен: TrimDuplicates: false выше выключает штатную
// дедупликацию Search (она иногда схлопывает нужные нам заявки), и тогда
// индекс может отдать один и тот же элемент несколькими хитами
// (переиндексация, несколько «версий» в crawl). Без этого фильтра хиты
// превращались в пункты комбобокса один-в-один, и заявка показывалась в
// списке дважды — с одинаковым React-ключом. Та же причина и то же лечение,
// что в request-portal (features/myRequests/myRequests.service.ts/hitsToEntries).
const seen = new Set<number>();
const ids = rawRows
.map(r => Number(r.ListItemID))
.filter(id => !isNaN(id) && id > 0)
.filter(id => {
if (seen.has(id)) return false;
seen.add(id);
return true;
});
if (ids.length === 0) return { items: [], hasMore: false };
const list = sp.web.getList(requestType.listId);
@@ -205,6 +223,8 @@ export class RequestManageService {
FIELDS.order,
FIELDS.allowReassign,
FIELDS.phase,
FIELDS.slaHours,
FIELDS.askOverdueReason,
`${FIELDS.approver}/Id`,
`${FIELDS.approver}/Title`,
`${FIELDS.approver}/EMail`
@@ -223,10 +243,15 @@ export class RequestManageService {
position: (r.Title as string) || '',
status: (r[FIELDS.status] as string) || '',
date: formatDateIso((r[FIELDS.date] as string) || ''),
dateIso: (r[FIELDS.date] as string) || '',
comment: (r[FIELDS.comment] as string) || '',
order: (r[FIELDS.order] as number) ?? i + 1,
allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false,
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval,
// ?? null, а не || null: ноль здесь — значимое значение («напоминать
// сразу»), и превращать его в «не задано» нельзя.
slaHours: (r[FIELDS.slaHours] as number | null) ?? null,
askOverdueReason: (r[FIELDS.askOverdueReason] as boolean | null) ?? null
};
});
@@ -256,6 +281,28 @@ export class RequestManageService {
});
}
/**
* Поправить SLA конкретной строки маршрута.
*
* null пишется в SharePoint как есть — это «в строке не задано», и тогда
* работает SLA типа заявки. Отдельный метод, а не флаг в reassignApprover:
* SLA правят независимо от того, кто на шаге, и в любом статусе строки.
*/
public async updateRowSla(
matrix: Pick<ManagedRequestMatrix, 'siteUrl' | 'approvalListTitle'>,
rowId: number,
// eslint-disable-next-line @rushstack/no-new-null -- null = «не задано», см. описание выше
slaHours: number | null,
// eslint-disable-next-line @rushstack/no-new-null -- null = «не задано»
askOverdueReason: boolean | null
): Promise<void> {
const sp = this.clientFor(matrix.siteUrl);
await sp.web.lists.getByTitle(matrix.approvalListTitle).items.getById(rowId).update({
[FIELDS.slaHours]: slaHours,
[FIELDS.askOverdueReason]: askOverdueReason
});
}
/** Удалить строку маршрута. */
public async removeRow(
matrix: Pick<ManagedRequestMatrix, 'siteUrl' | 'approvalListTitle'>,