feat: changed lists columns

This commit is contained in:
Тимур Городецкий
2026-08-06 15:36:22 +05:00
24 changed files with 1704 additions and 497 deletions
@@ -12,8 +12,12 @@ import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
// Внутренние имена колонок списка согласования (создаются провижинингом).
const FIELDS = {
approver: 'Approver', // «Согласующий» (User) → ApproverId
status: 'Status', // «Статус» (Choice)
// Внутренние имена с префиксом группы колонок сайта (TansuSite) и «Request» —
// так они заведены в SharePoint. Из них SharePoint выводит имена managed
// properties для поиска: TansuSiteApproverOWSUSER и RequestStatusOWSCHCS
// (см. MP_APPROVER/MP_STATUS в features/myRequests/myRequests.service.ts).
approver: 'TansuSiteApprover', // «Согласующий» (User) → TansuSiteApproverId
status: 'RequestStatus', // «Статус» (Choice)
ticket: 'Ticket', // «Заявка» (Lookup) → TicketId
date: 'Date', // «Дата» (DateTime)
comment: 'Comment', // «Комментарий» (Text)
@@ -28,6 +32,12 @@ export interface IApproverToWrite {
email: string;
position: string;
allowReassign: boolean;
/** Порядок (StepOrder). Не задан — берётся позиция в массиве (orderStart + индекс).
* Несколько согласующих с ОДИНАКОВЫМ order — параллельный шаг: все становятся
* «На согласовании» одновременно (см. createSteps). Сейчас редактор матрицы
* (ApprovalMatrix/useMatrixEditor) всегда даёт уникальный order по позиции —
* это поле для параллельных шагов, приходящих из внешнего маршрута напрямую. */
order?: number;
}
// Все запросы к списку «<форма> — согласование». listTitle задаётся формой.
@@ -38,9 +48,14 @@ export class ApprovalListService {
return getSP().web.lists.getByTitle(this.listTitle);
}
// Пишет по строке на каждого участника фазы с указанным статусом.
// Порядок (Order) = orderStart + позиция в массиве (фазы идут подряд в одном списке:
// сначала согласующие 1..N, затем исполнители N+1..N+M).
// Пишет по строке на каждого участника фазы. Порядок (Order) = a.order, если
// задан, иначе orderStart + позиция в массиве (фазы идут подряд в одном
// списке: сначала согласующие 1..N, затем исполнители N+1..N+M).
//
// Статус пишется переданным значением ОДИНАКОВО для всех строк партии — кто
// сейчас «на очереди», а кто «ждёт», решает отдельно activateNextQueued
// (вызывается сразу после createSteps в approvalSubmission.service.ts/writeRoute
// и в approverDecision.service.ts/decide).
public async createSteps(
requestId: number,
approvers: IApproverToWrite[],
@@ -48,22 +63,65 @@ export class ApprovalListService {
phase: string = APPROVAL_PHASE.approval,
orderStart: number = 1
): Promise<void> {
for (let i = 0; i < approvers.length; i++) {
const a = approvers[i];
if (!a.email) continue;
const withOrder = approvers
.map((a, i) => ({ a, order: a.order ?? orderStart + i }))
.filter((x) => !!x.a.email);
for (const { a, order } of withOrder) {
const approverId = await this.resolveUserId(a.email);
await this.list().items.add({
Title: a.position || a.email,
[`${FIELDS.approver}Id`]: approverId,
[FIELDS.status]: status,
[`${FIELDS.ticket}Id`]: requestId,
[FIELDS.order]: orderStart + i,
[FIELDS.order]: order,
[FIELDS.allowReassign]: a.allowReassign,
[FIELDS.phase]: phase,
});
}
}
// Продвигает очередь: среди ещё не активных строк со статусом «Ждёт очереди»
// в текущей фазе активирует те, что делят наименьший StepOrder (обычно одна
// строка; несколько — параллельный шаг). Фаза исполнения активируется, только
// когда фаза согласования пройдена целиком (без Отклонено/Возвращена) — иначе
// строки исполнения остаются «Ждёт очереди». Если фаза согласования уже
// остановлена решением, ничего не активирует.
public async activateNextQueued(requestId: number): Promise<void> {
const rows = await this.getForRequest(requestId);
const approvalRows = rows.filter((r) => r.phase !== APPROVAL_PHASE.execution);
const execRows = rows.filter((r) => r.phase === APPROVAL_PHASE.execution);
const approvalStopped = approvalRows.some(
(r) => r.status === REQUEST_STATUS.rejected || r.status === REQUEST_STATUS.returned
);
if (approvalStopped) return;
const approvalDone =
approvalRows.length === 0 || approvalRows.every((r) => r.status === REQUEST_STATUS.approved);
const activate = async (phaseRows: IApprovalRow[], activeStatus: string): Promise<void> => {
if (phaseRows.some((r) => r.status === activeStatus)) return;
const queuedRows = phaseRows.filter((r) => r.status === REQUEST_STATUS.queued);
if (queuedRows.length === 0) return;
const minOrder = Math.min(...queuedRows.map((r) => r.order));
await Promise.all(
queuedRows.filter((r) => r.order === minOrder).map((r) => this.setStatus(r.id, activeStatus))
);
};
if (!approvalDone) {
await activate(approvalRows, REQUEST_STATUS.onApproval);
} else {
await activate(execRows, REQUEST_STATUS.onExecution);
}
}
// Точечное обновление статуса одной строки маршрута.
public async setStatus(rowId: number, status: string): Promise<void> {
await this.list().items.getById(rowId).update({ [FIELDS.status]: status });
}
// Добавляет одну строку согласования с заданным Order (для делегата).
public async addApproverRow(
requestId: number,
@@ -187,19 +245,6 @@ export class ApprovalListService {
});
}
// Активирует следующий шаг маршрута: первую по очереди строку со статусом
// «Ждёт очереди» переводит в активный статус её фазы (Согласование → «На
// согласовании», Исполнение → «На исполнении»). Если ждущих нет — ничего.
// Вызывается при отправке (активировать первый шаг) и после каждого решения.
public async activateNextQueued(requestId: number): Promise<void> {
const rows = await this.getForRequest(requestId); // отсортированы по StepOrder
const next = rows.filter((r) => r.status === REQUEST_STATUS.queued)[0];
if (!next) return;
const active =
next.phase === APPROVAL_PHASE.execution ? REQUEST_STATUS.onExecution : REQUEST_STATUS.onApproval;
await this.list().items.getById(next.id).update({ [FIELDS.status]: active });
}
// Удаляет все строки согласования по заявке (при возврате/повторной отправке).
public async deleteForRequest(requestId: number): Promise<void> {
const rows = await this.getForRequest(requestId);
@@ -43,13 +43,24 @@ export async function writeApprovers(
status: string
): Promise<void> {
await approval.deleteForRequest(requestId);
await approval.createSteps(requestId, approvers, status, APPROVAL_PHASE.approval, 1);
const submitted = status === REQUEST_STATUS.onApproval;
const rowStatus = submitted ? REQUEST_STATUS.queued : status;
await approval.createSteps(requestId, approvers, rowStatus, APPROVAL_PHASE.approval, 1);
if (submitted) {
await approval.activateNextQueued(requestId);
}
}
// Записать ОБЕ фазы маршрута заявки: согласующих (Order 1..N) и исполнителей
// (Order N+1..N+M) в один список. Исполнители пишутся сразу при отправке —
// стороне согласующего внешний API уже не нужен, переход в фазу исполнения
// делает computeRequestStatus, когда все согласующие одобрили.
//
// Исполнители стартуют ВСЕ как «Ждёт очереди» (rowStatus, та же логика, что и
// у согласующих) — до прохождения фазы согласования у исполнителей нет текущей
// очереди. Первого исполнителя переключит на «На исполнении»
// approval.activateNextQueued, когда фаза согласования будет пройдена целиком
// (см. approverDecision.service.ts/decide).
export async function writeRoute(
approval: ApprovalListService,
requestId: number,
@@ -32,17 +32,20 @@ export class ApproverService {
) {}
// Ожидающая строка текущего пользователя, если сейчас его очередь; иначе undefined.
// Последовательное согласование: активна первая строка со статусом «На согласовании».
// Очередь уже проставлена на запись (см. createSteps/advanceQueue) — активна
// ровно одна строка со статусом «На согласовании» на фазу, искать «первую по
// порядку» не нужно.
public async getPendingRowForCurrentUser(requestId: number): Promise<IApprovalRow | undefined> {
const rows = await this.approval.getForRequest(requestId);
const userId = await this.approval.getCurrentUserId();
// Активна первая незакрытая строка по очереди: согласующий («На согласовании»)
// или, когда согласование пройдено, исполнитель («На исполнении»).
const firstPending = rows.filter(
// Активные строки текущего шага: согласующий («На согласовании») или, когда
// согласование пройдено, исполнитель («На исполнении»). При параллельном шаге
// (одинаковый StepOrder) активных строк может быть несколько сразу — ищем
// среди ВСЕХ активных, а не только среди первой по порядку.
const active = rows.filter(
(r) => r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution
)[0];
if (firstPending && firstPending.approverId === userId) return firstPending;
return undefined;
);
return active.filter((r) => r.approverId === userId)[0];
}
// Делегирование: согласующий добавляет делеганта ПЕРЕД собой (Order — между
@@ -59,22 +62,24 @@ export class ApproverService {
const prevOrder = before.length > 0 ? before[before.length - 1].order : current.order - 1;
const newOrder = (prevOrder + current.order) / 2;
const position = await this.approval.getJobTitle(user.loginName);
// Делегат наследует фазу той строки, вместо которой действует, и её статус
// ожидания (согласование → «На согласовании», исполнение → «На исполнении»).
const pendingStatus =
current.phase === APPROVAL_PHASE.execution
? REQUEST_STATUS.onExecution
: REQUEST_STATUS.onApproval;
// Делегат наследует фазу той строки, вместо которой согласует (согласование/исполнение).
// Обе строки — делегат и прежний согласующий — уходят в «Ждёт очереди»,
// activateNextQueued затем активирует делегата (у него меньший order).
await this.approval.addApproverRow(
requestId,
{ email: user.email, position, allowReassign: false },
newOrder,
pendingStatus,
REQUEST_STATUS.queued,
current.phase
);
await this.approval.setStatus(current.id, REQUEST_STATUS.queued);
await this.approval.activateNextQueued(requestId);
emitRequestsChanged();
}
// Применяет решение: обновляет строку и пересчитывает статус заявки.
// Применяет решение: обновляет строку, продвигает очередь (на следующего
// согласующего внутри фазы либо на первого исполнителя, если фаза согласования
// только что завершилась целиком) и пересчитывает статус заявки.
// Возвращает новый статус заявки.
public async decide(
requestId: number,
@@ -2,6 +2,7 @@
export {
REQUEST_STATUS,
REQUEST_STATUS_CHOICES,
APPROVAL_ROW_STATUS_CHOICES,
APPROVAL_PHASE,
APPROVAL_PHASE_CHOICES,
EDITABLE_STATUSES,
@@ -3,18 +3,28 @@
// В слое формы кодом ставятся Draft (черновик) и OnApproval (отправка на согласование);
// Approved/Rejected/Returned выставляются при решениях согласующих; Execution/Closed —
// зарезервированы под будущие этапы.
// Статусы СТРОК списка «— согласование» (не заявки в контентном списке):
// draft/rejected/returned — общие с заявкой; остальные — только для строк
// маршрута, по фазам:
// Согласование: queued («Ждёт очереди») → onApproval («На согласовании») → approved/rejected/returned.
// Исполнение: queued → onExecution («На исполнении») → executed («Выполнено»).
// Активация — только через ApprovalListService.activateNextQueued (вызывается
// при отправке заявки и после каждого положительного решения, см.
// approvalSubmission.service.ts/writeRoute, approverDecision.service.ts/decide):
// среди строк со статусом queued в текущей фазе находит наименьший StepOrder
// и переводит её (или несколько, если делят один order — параллельный шаг) в
// onApproval/onExecution. Так «чья сейчас очередь» — просто значение колонки
// Status, без пересчёта порядка на клиенте.
export const REQUEST_STATUS = {
draft: 'Черновик',
onApproval: 'На согласовании',
queued: 'Ждёт очереди',
onExecution: 'На исполнении',
executed: 'Выполнено',
approved: 'Согласовано',
rejected: 'Отклонено',
returned: 'Возвращена на доработку',
// Статус СТРОКИ маршрута, до которой ещё не дошла очередь (активен только текущий шаг).
queued: 'Ждёт очереди',
// Статусы СТРОК фазы исполнения: активная очередь исполнителя и выполнено.
onExecution: 'На исполнении',
executed: 'Выполнено',
// Статусы ЗАЯВКИ на этапе исполнения.
// Статус ЗАЯВКИ (не строки) на этапе исполнения.
execution: 'Выполнение',
closed: 'Закрыта',
} as const;
@@ -33,20 +43,40 @@ export function isEditableStatus(status: string | undefined): boolean {
return !status || EDITABLE_STATUSES.indexOf(status) !== -1;
}
// Полный набор значений для Choice-колонки Status при провижининге.
// Наборов значений ДВА, и они не совпадают — это разные сущности:
//
// 1) статус ЗАЯВКИ — колонка Status в списке «… — данные». Пишется формой
// (Черновик/На согласовании) и computeRequestStatus после каждого решения.
// «Ждёт очереди»/«На исполнении»/«Выполнено» здесь не бывают: это состояния
// отдельного шага маршрута, а не заявки целиком.
//
// 2) статус СТРОКИ МАРШРУТА — колонка RequestStatus в списке «… — согласование».
// Пишется createSteps/activateNextQueued/setDecision. «Выполнение»/«Закрыта»
// здесь не бывают: это агрегатные состояния заявки, у отдельного шага
// смысла не имеют.
//
// Держать их в синхроне с forms/*/provisioning.json и forms/*/approval/provisioning.json.
export const REQUEST_STATUS_CHOICES: string[] = [
REQUEST_STATUS.draft,
REQUEST_STATUS.onApproval,
REQUEST_STATUS.approved,
REQUEST_STATUS.rejected,
REQUEST_STATUS.returned,
REQUEST_STATUS.queued,
REQUEST_STATUS.onExecution,
REQUEST_STATUS.executed,
REQUEST_STATUS.execution,
REQUEST_STATUS.closed,
];
export const APPROVAL_ROW_STATUS_CHOICES: string[] = [
REQUEST_STATUS.draft,
REQUEST_STATUS.queued,
REQUEST_STATUS.onApproval,
REQUEST_STATUS.onExecution,
REQUEST_STATUS.approved,
REQUEST_STATUS.executed,
REQUEST_STATUS.rejected,
REQUEST_STATUS.returned,
];
// Фаза строки в списке маршрута: согласование (кто одобряет) и исполнение
// (кто выполняет после согласования). Обе фазы живут в одном списке «— согласование»,
// строки различаются колонкой Phase. Значения приходят из внешнего API как