Sla (#11)
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:
+63
-8
@@ -3,30 +3,59 @@ import { Employee } from '../../models';
|
||||
import { texts } from '../../texts';
|
||||
import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee';
|
||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
||||
import { Field, FormGrid, FormModal, SearchableSelect } from '../ui';
|
||||
import {
|
||||
boolToTriState,
|
||||
isValidSlaHours,
|
||||
parseSlaHours,
|
||||
slaHoursToInput,
|
||||
triStateToBool,
|
||||
TriState,
|
||||
TRI_STATE_OPTIONS
|
||||
} from '../../utils/sla';
|
||||
import { Field, FormGrid, FormModal, SearchableSelect, Select, TextInput } from '../ui';
|
||||
|
||||
export interface IManageRequestsApproverModalProps {
|
||||
title: string;
|
||||
initialEmail?: string;
|
||||
/** SLA строки в часах на момент открытия. null — в строке не задано, работает SLA типа заявки. */
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- null = «не задано», это не 0
|
||||
initialSlaHours: number | null;
|
||||
/** Опрос причины просрочки по строке. null — берётся от типа заявки. */
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- см. initialSlaHours
|
||||
initialAskOverdueReason: boolean | null;
|
||||
employees: Employee[];
|
||||
employeesLoading: boolean;
|
||||
/** Email'ы (в любом регистре), уже занятые другими шагами той же фазы — этот человек не может встать в фазу второй раз. */
|
||||
excludedEmails: string[];
|
||||
onSave: (email: string, position: string) => Promise<void>;
|
||||
onSave: (
|
||||
email: string,
|
||||
position: string,
|
||||
// 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>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Переназначение согласующего/исполнителя существующего шага (см.
|
||||
* ManageRequestsScreen). Должность НЕ вводится — берётся из справочника
|
||||
* сотрудников (api.tnsu.kz/Employee/All, см. Employee.position в
|
||||
* models/dictionaries.ts) на момент сохранения, отдельного поля для неё
|
||||
* в этой форме нет. Добавление НОВОГО шага через модалку не идёт — см.
|
||||
* Правка существующего шага: кто согласует и какой у шага SLA.
|
||||
*
|
||||
* Одна форма на оба вида правки намеренно — раньше SLA жил в отдельной модалке
|
||||
* со своей кнопкой-флажком в строке, и получалось два похожих диалога и лишняя
|
||||
* иконка в и без того плотном ряду действий. Правки применяются одним
|
||||
* сохранением (см. saveModal в ManageRequestsScreen).
|
||||
*
|
||||
* Должность НЕ вводится — берётся из справочника сотрудников
|
||||
* (api.tnsu.kz/Employee/All, см. Employee.position в models/dictionaries.ts) на
|
||||
* момент сохранения. Добавление НОВОГО шага через модалку не идёт — см.
|
||||
* плашку-пикер под таблицей в ManageRequestsPhaseTable.
|
||||
*/
|
||||
export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalProps> = ({
|
||||
title,
|
||||
initialEmail = '',
|
||||
initialSlaHours,
|
||||
initialAskOverdueReason,
|
||||
employees,
|
||||
employeesLoading,
|
||||
excludedEmails,
|
||||
@@ -34,6 +63,10 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
||||
onClose
|
||||
}) => {
|
||||
const [email, setEmail] = React.useState(initialEmail);
|
||||
const [slaHours, setSlaHours] = React.useState(() => slaHoursToInput(initialSlaHours));
|
||||
const [askOverdueReason, setAskOverdueReason] = React.useState<TriState>(
|
||||
() => boolToTriState(initialAskOverdueReason)
|
||||
);
|
||||
const submit = useAsyncSubmit();
|
||||
|
||||
// Тот, кто уже занят в этой фазе, исключён из списка выбора — дубликат
|
||||
@@ -51,8 +84,14 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
||||
submit.fail(texts.manageRequests.validationDuplicateApprover);
|
||||
return;
|
||||
}
|
||||
if (!isValidSlaHours(slaHours)) {
|
||||
submit.fail(texts.manageRequests.validationSlaHours);
|
||||
return;
|
||||
}
|
||||
const position = findEmployeeByEmail(employees, trimmedEmail)?.position || '';
|
||||
submit.run(() => onSave(trimmedEmail, position));
|
||||
submit.run(() =>
|
||||
onSave(trimmedEmail, position, parseSlaHours(slaHours), triStateToBool(askOverdueReason))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -69,6 +108,22 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
||||
onChange={setEmail}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.manageRequests.slaHoursLabel} hint={texts.manageRequests.slaHoursHint}>
|
||||
<TextInput
|
||||
type="number"
|
||||
min={0}
|
||||
value={slaHours}
|
||||
onChange={e => setSlaHours(e.target.value)}
|
||||
placeholder={texts.manageRequests.slaHoursPlaceholder}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.manageRequests.askOverdueReasonLabel} hint={texts.manageRequests.askOverdueReasonHint}>
|
||||
<Select
|
||||
options={TRI_STATE_OPTIONS}
|
||||
value={askOverdueReason}
|
||||
onChange={e => setAskOverdueReason(e.target.value as TriState)}
|
||||
/>
|
||||
</Field>
|
||||
</FormGrid>
|
||||
</FormModal>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,23 @@ import { texts } from '../../texts';
|
||||
import { toEmployeeOption } from '../../utils/employee';
|
||||
import { blockIndexOf, blockRanges, markParallel, withOrdersFrom } from '../../utils/orderBlocks';
|
||||
import { ActionsCell, Badge, BadgeVariant, CellNote, IconButton, PrimaryCell, SearchableSelect } from '../ui';
|
||||
import { effectiveSlaHours, formatSlaHours, isRowOverdue } from '../../utils/sla';
|
||||
|
||||
export interface IManageRequestsPhaseTableProps {
|
||||
title: string;
|
||||
/** Строки ОДНОЙ фазы, уже отсортированные по order (см. ManageRequestsScreen). */
|
||||
rows: ManagedApprovalRow[];
|
||||
/**
|
||||
* ВСЕ строки заявки (обе фазы) — нужны для расчёта момента активации шага
|
||||
* (см. utils/sla.ts/activatedAt): первый шаг исполнения отсчитывается от
|
||||
* последнего решения фазы согласования, а той фазы в `rows` этой таблицы нет.
|
||||
*/
|
||||
allRows: ManagedApprovalRow[];
|
||||
/** Дата создания заявки (ISO) — точка отсчёта для самого первого шага согласования. */
|
||||
requestCreatedIso: string;
|
||||
/** SLA типа заявки в часах — то, что действует, если на строке своё значение не задано. */
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- зеркалит nullable-контракт бэкенда
|
||||
requestTypeSlaHours: number | null;
|
||||
/** Заявка целиком только для просмотра (закрыта) — контролов нет вообще. */
|
||||
readOnly: boolean;
|
||||
/**
|
||||
@@ -75,6 +87,9 @@ function diffOrders(original: ManagedApprovalRow[], renumbered: Row[]): Array<{
|
||||
export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps> = ({
|
||||
title,
|
||||
rows,
|
||||
allRows,
|
||||
requestCreatedIso,
|
||||
requestTypeSlaHours,
|
||||
readOnly,
|
||||
requestRejected,
|
||||
statusVariant,
|
||||
@@ -187,6 +202,7 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
||||
<th>{texts.manageRequests.columnPosition}</th>
|
||||
<th>{texts.manageRequests.columnStatus}</th>
|
||||
<th>{texts.manageRequests.columnDate}</th>
|
||||
<th>{texts.manageRequests.columnSla}</th>
|
||||
{!readOnly && <th />}
|
||||
{!readOnly && <th />}
|
||||
</tr>
|
||||
@@ -244,6 +260,22 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
||||
<td>{r.position || texts.common.empty}</td>
|
||||
<td><Badge variant={statusVariant(r.status)}>{r.status || texts.common.empty}</Badge></td>
|
||||
<td>{r.date || texts.common.empty}</td>
|
||||
{(() => {
|
||||
const hours = effectiveSlaHours(r, requestTypeSlaHours);
|
||||
// Просроченным может быть только шаг, у которого прямо сейчас
|
||||
// идёт очередь — решённый уже не тикает, ждущий ещё не начался.
|
||||
const overdue =
|
||||
(r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution) &&
|
||||
isRowOverdue(r, allRows, requestCreatedIso, requestTypeSlaHours, new Date());
|
||||
return (
|
||||
<td className={overdue ? styles.slaOverdue : undefined}>
|
||||
{formatSlaHours(hours)}
|
||||
{r.slaHours === null && hours !== null && (
|
||||
<CellNote>{texts.manageRequests.slaInherited}</CellNote>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})()}
|
||||
{/* Стрелки перестановки — СЛЕВА от действий над строкой (объединить/
|
||||
переназначить/удалить), одна пара на весь блок (rowSpan): двигают
|
||||
шаг целиком, а не отдельного участника параллельной группы. */}
|
||||
|
||||
+8
@@ -97,3 +97,11 @@
|
||||
.addRow {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
// Ячейка SLA просроченного АКТИВНОГО шага (см. isRowOverdue в utils/sla.ts) —
|
||||
// только текст, без фона: фон был бы третьим одновременным акцентом в строке
|
||||
// (жёлтая подсветка hover уже есть) и мешал бы, а не помогал заметить.
|
||||
.slaOverdue {
|
||||
color: $red-500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -366,21 +366,40 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
// загруженного справочника сотрудников (тот же источник, что подставляет
|
||||
// должность, — см. ManageRequestsApproverModal), чтобы не делать лишний
|
||||
// запрос только за отображаемым именем.
|
||||
const saveModal = (email: string, position: string): Promise<void> => {
|
||||
const saveModal = (
|
||||
email: string,
|
||||
position: string,
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- null = «не задано», работает SLA типа заявки
|
||||
slaHours: number | null,
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- см. выше
|
||||
askOverdueReason: boolean | null
|
||||
): Promise<void> => {
|
||||
if (!matrix || !modal) return Promise.resolve();
|
||||
setSaveError(undefined);
|
||||
const rowId = modal.id;
|
||||
// Два запроса последовательно, а не параллельно: оба правят ОДНУ строку
|
||||
// SharePoint, и параллельные update по одному элементу списка могут
|
||||
// затереть друг друга (последний выигрывает).
|
||||
return requestManage
|
||||
.reassignApprover(matrix, rowId, email, position)
|
||||
.then(() => requestManage.updateRowSla(matrix, rowId, slaHours, askOverdueReason))
|
||||
.then(() => {
|
||||
const found = findEmployeeByEmail(employees.items, email);
|
||||
setMatrix(prev =>
|
||||
prev ? patchRow(prev, rowId, { approverEmail: email, approverName: found?.fio || email, position }) : prev
|
||||
prev
|
||||
? patchRow(prev, rowId, {
|
||||
approverEmail: email,
|
||||
approverName: found?.fio || email,
|
||||
position,
|
||||
slaHours,
|
||||
askOverdueReason
|
||||
})
|
||||
: prev
|
||||
);
|
||||
closeModal();
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
setSaveError(reportError(e, 'saveModal: reassignApprover'));
|
||||
setSaveError(reportError(e, 'saveModal: reassignApprover/updateRowSla'));
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
@@ -421,10 +440,15 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
position,
|
||||
status: initialStatus,
|
||||
date: '',
|
||||
dateIso: '',
|
||||
comment: '',
|
||||
order,
|
||||
allowReassign: false,
|
||||
phase
|
||||
phase,
|
||||
// Шаг, добавленный вручную, маршрутом-шаблоном не порождён и своего SLA не
|
||||
// имеет — работает SLA типа заявки, пока его не зададут отдельно.
|
||||
slaHours: null,
|
||||
askOverdueReason: null
|
||||
};
|
||||
|
||||
const previous = matrix;
|
||||
@@ -544,6 +568,9 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
<ManageRequestsPhaseTable
|
||||
title={texts.manageRequests.approvalPhaseTitle}
|
||||
rows={approvalRows}
|
||||
allRows={matrix ? matrix.rows : []}
|
||||
requestCreatedIso={matrix ? matrix.request.created : ''}
|
||||
requestTypeSlaHours={requestType?.slaHours ?? null}
|
||||
readOnly={readOnly}
|
||||
requestRejected={requestRejected}
|
||||
statusVariant={statusVariant}
|
||||
@@ -560,6 +587,9 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
<ManageRequestsPhaseTable
|
||||
title={texts.manageRequests.executionPhaseTitle}
|
||||
rows={executionRows}
|
||||
allRows={matrix ? matrix.rows : []}
|
||||
requestCreatedIso={matrix ? matrix.request.created : ''}
|
||||
requestTypeSlaHours={requestType?.slaHours ?? null}
|
||||
readOnly={readOnly}
|
||||
requestRejected={requestRejected}
|
||||
statusVariant={statusVariant}
|
||||
@@ -582,6 +612,8 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
||||
<ManageRequestsApproverModal
|
||||
title={texts.manageRequests.reassignTitle}
|
||||
initialEmail={modal.approverEmail}
|
||||
initialSlaHours={modal.slaHours}
|
||||
initialAskOverdueReason={modal.askOverdueReason}
|
||||
employees={employees.items}
|
||||
employeesLoading={employees.isLoading}
|
||||
excludedEmails={reassignExcludedEmails}
|
||||
|
||||
@@ -3,6 +3,16 @@ import { ApprovalRouteApi } from '../../services/api';
|
||||
import { formatSiteLabel, SharePointListsClient, SharePointSiteSuggestion } from '../../services/sharePointLists';
|
||||
import { RequestType } from '../../models';
|
||||
import { texts } from '../../texts';
|
||||
import {
|
||||
boolToTriState,
|
||||
formatSlaHours,
|
||||
isValidSlaHours,
|
||||
parseSlaHours,
|
||||
slaHoursToInput,
|
||||
triStateToBool,
|
||||
TriState,
|
||||
TRI_STATE_OPTIONS
|
||||
} from '../../utils/sla';
|
||||
import { useRequestTypes } from '../../hooks/useRequestTypes';
|
||||
import { useSharePointLists } from '../../hooks/useSharePointLists';
|
||||
import { useSharePointSiteTitle } from '../../hooks/useSharePointSiteTitle';
|
||||
@@ -38,9 +48,14 @@ interface IFormState {
|
||||
section: string;
|
||||
siteUrl: string;
|
||||
listId: string;
|
||||
/** Строкой, а не числом: пустое поле — «SLA не настроен», и это не то же самое, что 0. */
|
||||
slaHours: string;
|
||||
askOverdueReason: TriState;
|
||||
}
|
||||
|
||||
const EMPTY_FORM: IFormState = { name: '', pageUrl: '', section: '', siteUrl: '', listId: '' };
|
||||
const EMPTY_FORM: IFormState = {
|
||||
name: '', pageUrl: '', section: '', siteUrl: '', listId: '', slaHours: '', askOverdueReason: ''
|
||||
};
|
||||
|
||||
// siteUrl/listId — поля RequestType, зеркалящие nullable JSON-контракт бэкенда
|
||||
// (см. models/requestTypes.ts) — здесь сразу приводим null к undefined,
|
||||
@@ -88,7 +103,9 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
pageUrl: item.pageUrl,
|
||||
section: item.section,
|
||||
siteUrl: item.siteUrl ?? '',
|
||||
listId: item.listId ?? ''
|
||||
listId: item.listId ?? '',
|
||||
slaHours: slaHoursToInput(item.slaHours),
|
||||
askOverdueReason: boolToTriState(item.askOverdueReason)
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
@@ -104,6 +121,10 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
submit.fail(texts.requestTypes.validationListReference);
|
||||
return;
|
||||
}
|
||||
if (!isValidSlaHours(form.slaHours)) {
|
||||
submit.fail(texts.requestTypes.validationSlaHours);
|
||||
return;
|
||||
}
|
||||
|
||||
const input = {
|
||||
name: form.name.trim(),
|
||||
@@ -114,7 +135,9 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
// eslint-disable-next-line @rushstack/no-new-null
|
||||
siteUrl: siteUrl || null,
|
||||
// eslint-disable-next-line @rushstack/no-new-null
|
||||
listId: listId || null
|
||||
listId: listId || null,
|
||||
slaHours: parseSlaHours(form.slaHours),
|
||||
askOverdueReason: triStateToBool(form.askOverdueReason)
|
||||
};
|
||||
const request = editing ? api.requestTypes.update(editing.id, input) : api.requestTypes.create(input);
|
||||
|
||||
@@ -147,6 +170,7 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
{ key: 'name', header: texts.requestTypes.columnName, render: r => <PrimaryCell>{r.name}</PrimaryCell> },
|
||||
{ key: 'url', header: texts.requestTypes.columnPageUrl, render: r => r.pageUrl },
|
||||
{ key: 'section', header: texts.requestTypes.columnSection, render: r => r.section },
|
||||
{ key: 'sla', header: texts.requestTypes.columnSla, render: r => formatSlaHours(r.slaHours) },
|
||||
{ key: 'listId', header: texts.requestTypes.columnListId, render: r => <ListIdCell client={sharePoint} siteUrl={r.siteUrl} listId={r.listId} /> },
|
||||
{ key: 'siteUrl', header: texts.requestTypes.columnSiteUrl, render: r => <SiteUrlCell client={sharePoint} siteUrl={r.siteUrl} /> },
|
||||
{
|
||||
@@ -238,6 +262,22 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
onChange={e => setForm({ ...form, listId: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.requestTypes.slaHoursLabel} hint={texts.requestTypes.slaHoursHint}>
|
||||
<TextInput
|
||||
type="number"
|
||||
min={0}
|
||||
value={form.slaHours}
|
||||
onChange={e => setForm({ ...form, slaHours: e.target.value })}
|
||||
placeholder={texts.requestTypes.slaHoursPlaceholder}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.requestTypes.askOverdueReasonLabel} hint={texts.requestTypes.askOverdueReasonHint}>
|
||||
<Select
|
||||
options={TRI_STATE_OPTIONS}
|
||||
value={form.askOverdueReason}
|
||||
onChange={e => setForm({ ...form, askOverdueReason: e.target.value as TriState })}
|
||||
/>
|
||||
</Field>
|
||||
</FormGrid>
|
||||
</FormModal>
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,15 @@ import { useSharePointFields } from '../../hooks/useSharePointFields';
|
||||
import { ApprovalRouteApi } from '../../services/api';
|
||||
import { isNumericFieldType, SharePointListsClient } from '../../services/sharePointLists';
|
||||
import { Checkbox, Field, FormGrid, FormModal, RadioGroup, SearchableSelect, Select, TextInput } from '../ui';
|
||||
import {
|
||||
boolToTriState,
|
||||
isValidSlaHours,
|
||||
parseSlaHours,
|
||||
slaHoursToInput,
|
||||
triStateToBool,
|
||||
TriState,
|
||||
TRI_STATE_OPTIONS
|
||||
} from '../../utils/sla';
|
||||
|
||||
export interface IRouteStepFormModalProps {
|
||||
api: ApprovalRouteApi;
|
||||
@@ -47,6 +56,9 @@ interface IFormState {
|
||||
fieldName: string;
|
||||
operation: ComparisonOperator;
|
||||
value: string;
|
||||
/** Пусто — наследовать SLA от типа заявки; «0» — напоминать сразу. Это разные вещи. */
|
||||
slaHours: string;
|
||||
askOverdueReason: TriState;
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution]
|
||||
@@ -78,7 +90,9 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number
|
||||
hasCondition: false,
|
||||
fieldName: '',
|
||||
operation: ComparisonOperator.GreaterThan,
|
||||
value: ''
|
||||
value: '',
|
||||
slaHours: '',
|
||||
askOverdueReason: ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -93,7 +107,9 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number
|
||||
hasCondition: Boolean(editing.fieldName),
|
||||
fieldName: editing.fieldName ?? '',
|
||||
operation: editing.operation && editing.operation !== ComparisonOperator.None ? editing.operation : ComparisonOperator.GreaterThan,
|
||||
value: editing.value ?? ''
|
||||
value: editing.value ?? '',
|
||||
slaHours: slaHoursToInput(editing.slaHours),
|
||||
askOverdueReason: boolToTriState(editing.askOverdueReason)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -164,6 +180,10 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
submit.fail(texts.routeSteps.validationConditionNumber);
|
||||
return;
|
||||
}
|
||||
if (!isValidSlaHours(form.slaHours)) {
|
||||
submit.fail(texts.routeSteps.validationSlaHours);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
orderNo,
|
||||
@@ -175,7 +195,9 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
headType: form.kind === 'headType' ? form.headType : null,
|
||||
fieldName: form.hasCondition ? form.fieldName.trim() : null,
|
||||
operation: form.hasCondition ? form.operation : null,
|
||||
value: form.hasCondition ? form.value.trim() : null
|
||||
value: form.hasCondition ? form.value.trim() : null,
|
||||
slaHours: parseSlaHours(form.slaHours),
|
||||
askOverdueReason: triStateToBool(form.askOverdueReason)
|
||||
};
|
||||
|
||||
const request = editing
|
||||
@@ -283,6 +305,23 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={texts.routeSteps.slaHoursLabel} hint={texts.routeSteps.slaHoursHint}>
|
||||
<TextInput
|
||||
type="number"
|
||||
min={0}
|
||||
value={form.slaHours}
|
||||
onChange={e => setForm({ ...form, slaHours: e.target.value })}
|
||||
placeholder={texts.routeSteps.slaHoursPlaceholder}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.routeSteps.askOverdueReasonLabel} hint={texts.routeSteps.askOverdueReasonHint}>
|
||||
<Select
|
||||
options={TRI_STATE_OPTIONS}
|
||||
value={form.askOverdueReason}
|
||||
onChange={e => setForm({ ...form, askOverdueReason: e.target.value as TriState })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={texts.routeSteps.conditionLabel} full hint={!hasListId ? texts.routeSteps.conditionDisabledHint : undefined}>
|
||||
<Checkbox
|
||||
label={texts.routeSteps.conditionCheckbox}
|
||||
|
||||
Reference in New Issue
Block a user