Merge branch 'main' of http://git.tnsu.kz/timur.gorodetskiy/request-portal
Tests / frontend-test (push) Successful in 1m14s

This commit is contained in:
Тимур Городецкий
2026-09-14 16:05:37 +05:00
31 changed files with 78 additions and 11 deletions
@@ -201,7 +201,8 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -199,7 +199,8 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -222,7 +222,8 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -46,7 +46,8 @@ function toApprovers(rows: IMatrixRow[]): IApproverToWrite[] {
allowReassign: r.allowReassign,
order: r.order,
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
}
@@ -188,7 +188,8 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -214,6 +214,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
// Удаление уже прикреплённого файла (в правке) — сразу из заявки.
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -224,7 +224,8 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const handleRemoveAttachment = (fileName: string): void => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -268,7 +268,8 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -208,7 +208,8 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const handleRemoveAttachment = (fileName: string): void => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -200,7 +200,8 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -198,7 +198,8 @@ const VacationForm: React.FC<IRequestFormProps> = ({
// Транзитом из маршрута в строку списка: наследование бэкенд применил
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -232,6 +232,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
// при resolve, форма только переносит значения (см. IMatrixRow.slaHours).
slaHours: r.slaHours,
askOverdueReason: r.askOverdueReason,
autoApprove: r.autoApprove
}));
// Удаление уже прикреплённого файла (в правке) — сразу из заявки.
@@ -11,6 +11,7 @@
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] },
{ "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" },
{ "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" },
{ "internalName": "AutoApprove", "title": "Автосогласование", "type": "Boolean" },
{ "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" }
]
}
@@ -36,6 +36,7 @@ const FIELDS = {
delegatedBy: 'DelegatedBy', // «Кем переназначено» (User) → DelegatedById
slaHours: 'SLAhours', // «SLA, часов» (Number) — пусто значит «взять у типа заявки»
askOverdueReason: 'AskOverdueReason', // «Опрос причины просрочки» (Да/Нет)
autoApprove: 'AutoApprove', // «Автосогласование» (Да/Нет) — см. IApproverToWrite.autoApprove
};
// Согласующий для записи в список.
@@ -61,6 +62,14 @@ export interface IApproverToWrite {
slaHours?: number | null;
// eslint-disable-next-line @rushstack/no-new-null -- см. slaHours
askOverdueReason?: boolean | null;
/**
* Согласовать шаг автоматически по истечении SLA — так же, как SLA-поля выше,
* приходит из resolve с уже применённым решением (наследовать здесь нечего,
* см. ResolvedRouteStepDto.AutoApprove на бэкенде), портал только переносит
* значение в строку. undefined — «не задано» (старые строки без этого поля);
* фактическое решение принимает не портал, а фоновая задача бэкенда.
*/
autoApprove?: boolean;
}
// Все запросы к списку «<форма> — согласование». listTitle задаётся формой.
@@ -141,6 +150,7 @@ export class ApprovalListService {
// ?? null, а не || null: ноль часов — значимое значение.
[FIELDS.slaHours]: a.slaHours ?? null,
[FIELDS.askOverdueReason]: a.askOverdueReason ?? null,
[FIELDS.autoApprove]: a.autoApprove ?? false,
});
});
await execute();
@@ -278,6 +288,7 @@ export class ApprovalListService {
FIELDS.phase,
FIELDS.slaHours,
FIELDS.askOverdueReason,
FIELDS.autoApprove,
`${FIELDS.approver}/Title`,
`${FIELDS.approver}/EMail`,
`${FIELDS.delegatedBy}/Title`
@@ -306,6 +317,7 @@ export class ApprovalListService {
// проставленный при первой отправке, обнулился бы у черновика.
slaHours: (r[FIELDS.slaHours] as number | null) ?? null,
askOverdueReason: (r[FIELDS.askOverdueReason] as boolean | null) ?? null,
autoApprove: (r[FIELDS.autoApprove] as boolean | null) ?? false,
};
});
}
@@ -38,6 +38,7 @@ export async function getPlannedMatrix(
phase,
slaHours: s.slaHours,
askOverdueReason: s.askOverdueReason,
autoApprove: s.autoApprove,
warning: s.warning,
}))
);
@@ -108,4 +108,13 @@ export interface IResolvedRouteStep {
slaHours: number | null;
// eslint-disable-next-line @rushstack/no-new-null -- см. slaHours
askOverdueReason: boolean | null;
/**
* Согласовать шаг автоматически, когда истечёт его SLA — настраивается ТОЛЬКО в
* админке маршрутов (approval-route), портал это значение только читает из
* resolve и переносит в строку списка согласования, ничего не решая сам:
* фактическое автосогласование выполняет фоновая задача бэкенда (Hangfire).
* Обычный boolean, а не nullable, как SLA-поля выше, — наследовать здесь
* нечего, см. соответствующее поле на бэкенде (TemplateRouteStep.AutoApprove).
*/
autoApprove: boolean;
}
@@ -31,6 +31,13 @@ export interface IMatrixRow {
slaHours?: number | null;
// eslint-disable-next-line @rushstack/no-new-null -- см. slaHours
askOverdueReason?: boolean | null;
/**
* Согласовать шаг автоматически по истечении SLA — то же транзитное поле, что и
* slaHours/askOverdueReason выше: настраивается в админке, портал только
* переносит значение в строку списка, ничего не решая сам (см.
* IResolvedRouteStep.autoApprove).
*/
autoApprove?: boolean;
/**
* Предупреждение approval-route по ЭТОМУ шагу (см. IResolvedRouteStep.warning) —
* например, роль не разрешилась в конкретного человека, и почему именно.
@@ -193,7 +193,12 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
)}
{hasDates && <td data-label="Дата">{r.date}</td>}
{hasSla && (
<td data-label="SLA">{r.slaHours || r.slaHours === 0 ? `${r.slaHours} ч` : '—'}</td>
<td data-label="SLA">
{r.slaHours || r.slaHours === 0 ? `${r.slaHours} ч` : '—'}
{/* Настраивается только в админке — здесь чисто информативно,
чтобы участник видел: решение по шагу примут не только вручную. */}
{r.autoApprove && <span className={styles.autoApproveBadge}> (авто)</span>}
</td>
)}
{/* Перемещение действует на ШАГ целиком (параллельные строки
переезжают вместе): одна пара стрелок на весь шаг. */}
@@ -187,6 +187,14 @@
font-weight: 500;
}
// Пометка «(авто)» рядом со значением SLA — шаг настроен на автосогласование
// (см. ApprovalMatrix.tsx/r.autoApprove). Нейтральным серым, не золотым и не
// красным: это не проблема и не решение человека, а просто информация о
// правиле, действующем на шаг.
.autoApproveBadge {
color: $gray-500;
}
// Причина, по которой approval-route не назначил человека на шаг (см.
// ApprovalMatrix.tsx/r.warning) — красным, как настоящая проблема маршрута
// (в отличие от нейтрального addedByInline/commentInline): пустая строка