feat(автосогласование): флаг «Автосогласование» в форме шага маршрута
Tests / backend-test (pull_request) Successful in 2m37s
Tests / frontend-test (pull_request) Successful in 4m2s

Админка получает управление флагом AutoApprove на шаге маршрута (добавлен в
предыдущем коммите на бэкенде): чекбокс в RouteStepFormModal, перенос значения
при копировании маршрута (RouteCopyModal), проверка «включён без действующего
SLA — ошибка» (считать нечего, задача такой шаг пропустит молча).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-09-14 09:17:51 +05:00
parent ad62ac96ac
commit a1d29a6851
5 changed files with 43 additions and 4 deletions
@@ -47,7 +47,8 @@ function toStepInput(step: TemplateRouteStep): CreateTemplateRouteStepInput {
conditions: step.conditions, conditions: step.conditions,
conditionsLogic: step.conditionsLogic, conditionsLogic: step.conditionsLogic,
slaHours: step.slaHours, slaHours: step.slaHours,
askOverdueReason: step.askOverdueReason askOverdueReason: step.askOverdueReason,
autoApprove: step.autoApprove
}; };
} }
@@ -77,6 +77,7 @@ interface IFormState {
/** Пусто — наследовать SLA от типа заявки; «0» — напоминать сразу. Это разные вещи. */ /** Пусто — наследовать SLA от типа заявки; «0» — напоминать сразу. Это разные вещи. */
slaHours: string; slaHours: string;
askOverdueReason: TriState; askOverdueReason: TriState;
autoApprove: boolean;
} }
const EMPTY_CONDITION: RouteStepCondition = { fieldName: '', operation: ComparisonOperator.GreaterThan, value: '' }; const EMPTY_CONDITION: RouteStepCondition = { fieldName: '', operation: ComparisonOperator.GreaterThan, value: '' };
@@ -133,7 +134,8 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number
conditions: [EMPTY_CONDITION], conditions: [EMPTY_CONDITION],
conditionsLogic: ConditionsLogic.And, conditionsLogic: ConditionsLogic.And,
slaHours: '', slaHours: '',
askOverdueReason: '' askOverdueReason: '',
autoApprove: false
}; };
} }
return { return {
@@ -149,7 +151,8 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number
conditions: editing.conditions.length > 0 ? editing.conditions : [EMPTY_CONDITION], conditions: editing.conditions.length > 0 ? editing.conditions : [EMPTY_CONDITION],
conditionsLogic: editing.conditionsLogic, conditionsLogic: editing.conditionsLogic,
slaHours: slaHoursToInput(editing.slaHours), slaHours: slaHoursToInput(editing.slaHours),
askOverdueReason: boolToTriState(editing.askOverdueReason) askOverdueReason: boolToTriState(editing.askOverdueReason),
autoApprove: editing.autoApprove
}; };
} }
@@ -365,6 +368,14 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
submit.fail(texts.routeSteps.validationSlaHours); submit.fail(texts.routeSteps.validationSlaHours);
return; return;
} }
// Автосогласование отсчитывается от SLA — без него фоновая задача шаг просто
// пропустит (см. AutoApproveService.DeadlineOf), и настройка тихо не сработала
// бы. Пустое поле здесь не значит «нет SLA»: оно значит «взять у типа заявки»,
// поэтому ошибка только когда SLA нет и там.
if (form.autoApprove && parseSlaHours(form.slaHours) === null && requestType?.slaHours === null) {
submit.fail(texts.routeSteps.validationAutoApproveWithoutSla);
return;
}
const payload = { const payload = {
orderNo, orderNo,
@@ -379,7 +390,8 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
: null, : null,
conditionsLogic: form.conditionsLogic, conditionsLogic: form.conditionsLogic,
slaHours: parseSlaHours(form.slaHours), slaHours: parseSlaHours(form.slaHours),
askOverdueReason: triStateToBool(form.askOverdueReason) askOverdueReason: triStateToBool(form.askOverdueReason),
autoApprove: form.autoApprove
}; };
const request = editing const request = editing
@@ -505,6 +517,14 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
/> />
</Field> </Field>
<Field label={texts.routeSteps.autoApproveLabel} full hint={texts.routeSteps.autoApproveHint}>
<Checkbox
label={texts.routeSteps.autoApproveCheckbox}
checked={form.autoApprove}
onChange={v => setForm({ ...form, autoApprove: v })}
/>
</Field>
<Field label={texts.routeSteps.conditionLabel} full hint={!hasListId ? texts.routeSteps.conditionDisabledHint : undefined}> <Field label={texts.routeSteps.conditionLabel} full hint={!hasListId ? texts.routeSteps.conditionDisabledHint : undefined}>
<Checkbox <Checkbox
label={texts.routeSteps.conditionCheckbox} label={texts.routeSteps.conditionCheckbox}
@@ -34,6 +34,15 @@ export interface TemplateRouteStep {
slaHours: number | null; slaHours: number | null;
/** Спрашивать ли причину просрочки на этом шаге. null — наследовать от типа. */ /** Спрашивать ли причину просрочки на этом шаге. null — наследовать от типа. */
askOverdueReason: boolean | null; askOverdueReason: boolean | null;
/**
* Согласовывать шаг автоматически, когда истечёт его SLA (фоновая задача бэкенда,
* см. AutoApproveService). Обычный boolean, а не «три состояния», как у полей выше:
* наследовать его неоткуда — у типа заявки такой настройки нет.
*
* Работает только вместе с SLA: без действующего SLA (ни здесь, ни у типа заявки)
* отсчитывать нечего, и задача такой шаг пропускает.
*/
autoApprove: boolean;
/** Предупреждение бэкенда о сохранённом шаге (например, должность не найдена). */ /** Предупреждение бэкенда о сохранённом шаге (например, должность не найдена). */
warning?: string | null; warning?: string | null;
} }
@@ -50,6 +59,7 @@ export interface CreateTemplateRouteStepInput {
conditionsLogic: ConditionsLogic; conditionsLogic: ConditionsLogic;
slaHours: number | null; slaHours: number | null;
askOverdueReason: boolean | null; askOverdueReason: boolean | null;
autoApprove: boolean;
} }
export type UpdateTemplateRouteStepInput = CreateTemplateRouteStepInput; export type UpdateTemplateRouteStepInput = CreateTemplateRouteStepInput;
@@ -66,6 +76,7 @@ export interface PatchTemplateRouteStepInput {
conditionsLogic?: ConditionsLogic; conditionsLogic?: ConditionsLogic;
slaHours?: number | null; slaHours?: number | null;
askOverdueReason?: boolean | null; askOverdueReason?: boolean | null;
autoApprove?: boolean;
} }
// Способ задать исполнителя шага: ровно одно из полей выше должно быть заполнено // Способ задать исполнителя шага: ровно одно из полей выше должно быть заполнено
@@ -329,6 +329,12 @@ export const texts = {
slaHoursPlaceholder: 'Как у типа заявки', slaHoursPlaceholder: 'Как у типа заявки',
askOverdueReasonLabel: 'Спрашивать причину просрочки', askOverdueReasonLabel: 'Спрашивать причину просрочки',
askOverdueReasonHint: '«Не задано» — берётся значение типа заявки.', askOverdueReasonHint: '«Не задано» — берётся значение типа заявки.',
autoApproveLabel: 'Автосогласование',
autoApproveCheckbox: 'Согласовать шаг автоматически, когда истечёт SLA',
autoApproveHint:
'Если согласующий не примет решение в срок, шаг будет согласован без него и заявка пойдёт дальше. Проверка идёт раз в час, поэтому согласование произойдёт в течение часа после истечения срока.',
validationAutoApproveWithoutSla:
'Автосогласование считается от SLA — задайте SLA шага или SLA типа заявки.',
conditionLabel: 'Условие видимости шага', conditionLabel: 'Условие видимости шага',
conditionCheckbox: 'Показывать шаг только при выполнении условия', conditionCheckbox: 'Показывать шаг только при выполнении условия',
conditionDisabledHint: conditionDisabledHint:
@@ -16,6 +16,7 @@ function step(overrides: Partial<TemplateRouteStep>): TemplateRouteStep {
conditionsLogic: ConditionsLogic.And, conditionsLogic: ConditionsLogic.And,
slaHours: null, slaHours: null,
askOverdueReason: null, askOverdueReason: null,
autoApprove: false,
...overrides ...overrides
}; };
} }