куча разных нюансов сделано
This commit is contained in:
@@ -68,11 +68,23 @@ export class ApprovalListService {
|
||||
// сквозную нумерацию). a.order — номер шага ВНУТРИ фазы (1..N), и у
|
||||
// параллельных шагов он совпадает, поэтому нумеровать по позиции в массиве
|
||||
// нельзя. Без a.order падаем на позицию — для вызовов, где параллельности нет.
|
||||
const withOrder = approvers
|
||||
.map((a, i) => ({ a, order: orderStart - 1 + (a.order ?? i + 1) }))
|
||||
.filter((x) => !!x.a.email);
|
||||
const withOrder = approvers.map((a, i) => ({ a, order: orderStart - 1 + (a.order ?? i + 1) }));
|
||||
|
||||
for (const { a, order } of withOrder) {
|
||||
// Строку без email в SharePoint не записать (Person-поле обязательного
|
||||
// значения) — раньше такие шаги пропадали отсюда молча, и согласующие в
|
||||
// середине маршрута просто не получали заявку (см. findUnassignedStep —
|
||||
// это должно было быть отловлено валидацией ДО вызова createSteps; лог
|
||||
// здесь — на случай, если её обошли, например прямым вызовом сервиса).
|
||||
const missing = withOrder.filter((x) => !x.a.email);
|
||||
if (missing.length > 0) {
|
||||
console.error(
|
||||
`[Согласование] createSteps: пропущены шаги без назначенного человека (order ${missing
|
||||
.map((x) => x.order)
|
||||
.join(', ')}) — заявка №${requestId}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const { a, order } of withOrder.filter((x) => !!x.a.email)) {
|
||||
const approverId = await this.resolveUserId(a.email);
|
||||
await this.list().items.add({
|
||||
Title: a.position || a.email,
|
||||
@@ -170,6 +182,12 @@ export class ApprovalListService {
|
||||
}
|
||||
|
||||
// Строки согласования для отображения матрицы (ФИО, должность, статус, дата, комментарий).
|
||||
//
|
||||
// Approver/EMail нужен не только для показа: при редактировании черновика
|
||||
// (см. useMatrixEditor с editable=true в WorkplaceSetupForm/RecruitingHireForm)
|
||||
// это ЕДИНСТВЕННЫЙ источник email для уже существующих строк — без него
|
||||
// findUnassignedStep решает, что назначенного человека нет, хотя он есть
|
||||
// (баг: пропущенный email тут выглядел так же, как реально пустой шаг).
|
||||
public async getMatrixForRequest(requestId: number): Promise<IMatrixRow[]> {
|
||||
const rows = (await this.list()
|
||||
.items.filter(`${FIELDS.ticket}Id eq ${requestId}`)
|
||||
@@ -182,17 +200,18 @@ export class ApprovalListService {
|
||||
FIELDS.order,
|
||||
FIELDS.allowReassign,
|
||||
FIELDS.phase,
|
||||
`${FIELDS.approver}/Title`
|
||||
`${FIELDS.approver}/Title`,
|
||||
`${FIELDS.approver}/EMail`
|
||||
)
|
||||
.expand(FIELDS.approver)
|
||||
.orderBy(FIELDS.order, true)()) as Array<Record<string, unknown>>;
|
||||
|
||||
return rows.map((r, i) => {
|
||||
const approver = r[FIELDS.approver] as { Title?: string } | undefined;
|
||||
const approver = r[FIELDS.approver] as { Title?: string; EMail?: string } | undefined;
|
||||
return {
|
||||
order: (r[FIELDS.order] as number) ?? i + 1,
|
||||
who: approver?.Title || '',
|
||||
email: '',
|
||||
email: approver?.EMail || '',
|
||||
position: (r.Title as string) || '',
|
||||
status: (r[FIELDS.status] as string) || '',
|
||||
date: this.formatDate((r[FIELDS.date] as string) || ''),
|
||||
|
||||
@@ -24,7 +24,7 @@ export type { IApproverToWrite } from './approvalList.service';
|
||||
export { ApproverService } from './approverDecision.service';
|
||||
export type { ApproverDecision } from './approverDecision.service';
|
||||
export { getPlannedMatrix, writeApprovers, writeRoute } from './approvalSubmission.service';
|
||||
export { useMatrixEditor } from './useMatrixEditor';
|
||||
export { useMatrixEditor, findUnassignedStep } from './useMatrixEditor';
|
||||
export type { IUseMatrixEditor } from './useMatrixEditor';
|
||||
export { ApprovalMatrix } from './ui/ApprovalMatrix';
|
||||
export { UserPicker } from './ui/UserPicker';
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
|
||||
.matrixActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -181,15 +182,16 @@
|
||||
|
||||
// Перемещение шага целиком — тоже объединённая по вертикали ячейка, поэтому
|
||||
// кнопки центрируем относительно всего блока.
|
||||
// Та же flex-раскладка, что и у .matrixActions — иначе кнопки здесь (обычные
|
||||
// инлайновые элементы) выравниваются по baseline и «прыгают» относительно
|
||||
// кнопок соседней колонки, которая уже flex-контейнер.
|
||||
.matrixBlockMove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: $sp-1;
|
||||
width: 64px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
text-align: right;
|
||||
|
||||
.iconBtn + .iconBtn {
|
||||
margin-left: $sp-1;
|
||||
}
|
||||
}
|
||||
|
||||
// Строки, входящие в шаг из нескольких параллельных согласующих — лёгкая
|
||||
|
||||
@@ -78,6 +78,16 @@ function markParallel(rows: IMatrixRow[]): IMatrixRow[] {
|
||||
}));
|
||||
}
|
||||
|
||||
// Первый шаг без назначенного человека (email пуст) — обычно шаг маршрута,
|
||||
// который внешний approval-route API вернул как роль без привязанного
|
||||
// исполнителя. Писать такую строку в SharePoint нельзя (Person-поле требует
|
||||
// значение) — approvalList.service.ts/createSteps молча пропускает её при
|
||||
// записи, поэтому перед отправкой это нужно поймать здесь и не дать заявке уйти
|
||||
// на согласование с пропущенным шагом (см. RecruitingHireForm/WorkplaceSetupForm).
|
||||
export function findUnassignedStep(rows: IMatrixRow[]): IMatrixRow | undefined {
|
||||
return rows.filter((r) => !r.email).sort((a, b) => a.order - b.order)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает матрицу согласования и (если editable) позволяет её редактировать.
|
||||
* @param load загрузчик строк (планируемый маршрут из resolve или строки заявки).
|
||||
|
||||
@@ -20,7 +20,9 @@ import * as React from 'react';
|
||||
// перезагрузки страницы. useRoute() подписан и на popstate (кнопка «назад»),
|
||||
// и на её собственное событие, поэтому реагирует на оба способа смены URL.
|
||||
|
||||
export type PortalMode = 'submit' | 'approve' | 'create';
|
||||
// 'edit' — открыть СВОЮ незавершённую заявку (черновик / возвращённую /
|
||||
// отклонённую) на редактирование в той же выезжающей панели, что и просмотр.
|
||||
export type PortalMode = 'submit' | 'approve' | 'create' | 'edit';
|
||||
|
||||
/** Раздел бокового меню. */
|
||||
export type NavSection = 'new' | 'approvals' | 'own' | 'search';
|
||||
@@ -29,9 +31,9 @@ export interface IRoute {
|
||||
mode: PortalMode;
|
||||
/** Раздел бокового меню; сохраняется и когда поверх открыта заявка. */
|
||||
nav: NavSection;
|
||||
/** Ключ формы (тип заявки) — для 'approve' и 'create'. */
|
||||
/** Ключ формы (тип заявки) — для 'approve', 'edit' и 'create'. */
|
||||
formKey?: string;
|
||||
/** Id заявки в списке данных — для 'approve'. */
|
||||
/** Id заявки в списке данных — для 'approve' и 'edit'. */
|
||||
itemId?: number;
|
||||
/** Id исходной заявки для предзаполнения — для 'create' (напр. орг. места на основе прием). */
|
||||
sourceId?: number;
|
||||
@@ -59,6 +61,10 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
||||
if (mode === 'approve' && formKey && itemId && !isNaN(itemId)) {
|
||||
return { mode: 'approve', nav, formKey, itemId };
|
||||
}
|
||||
// Редактирование собственной незавершённой заявки — та же панель, но с формой.
|
||||
if (mode === 'edit' && formKey && itemId && !isNaN(itemId)) {
|
||||
return { mode: 'edit', nav, formKey, itemId };
|
||||
}
|
||||
// Создание производной заявки с предзаполнением из исходной.
|
||||
if (formKey && sourceId && !isNaN(sourceId)) {
|
||||
return { mode: 'create', nav: 'new', formKey, sourceId };
|
||||
@@ -78,8 +84,8 @@ function buildQuery(route: IRoute): string {
|
||||
if (route.nav !== 'new') {
|
||||
params.set('rpnav', route.nav);
|
||||
}
|
||||
if (route.mode === 'approve' && route.formKey && route.itemId) {
|
||||
params.set('rpmode', 'approve');
|
||||
if ((route.mode === 'approve' || route.mode === 'edit') && route.formKey && route.itemId) {
|
||||
params.set('rpmode', route.mode);
|
||||
params.set('rptype', route.formKey);
|
||||
params.set('rpid', String(route.itemId));
|
||||
} else if (route.mode === 'create' && route.formKey && route.sourceId) {
|
||||
|
||||
Reference in New Issue
Block a user