все переехало в карандашик
This commit is contained in:
+63
-8
@@ -3,30 +3,59 @@ import { Employee } from '../../models';
|
|||||||
import { texts } from '../../texts';
|
import { texts } from '../../texts';
|
||||||
import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee';
|
import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee';
|
||||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
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 {
|
export interface IManageRequestsApproverModalProps {
|
||||||
title: string;
|
title: string;
|
||||||
initialEmail?: 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[];
|
employees: Employee[];
|
||||||
employeesLoading: boolean;
|
employeesLoading: boolean;
|
||||||
/** Email'ы (в любом регистре), уже занятые другими шагами той же фазы — этот человек не может встать в фазу второй раз. */
|
/** Email'ы (в любом регистре), уже занятые другими шагами той же фазы — этот человек не может встать в фазу второй раз. */
|
||||||
excludedEmails: string[];
|
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;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Переназначение согласующего/исполнителя существующего шага (см.
|
* Правка существующего шага: кто согласует и какой у шага SLA.
|
||||||
* ManageRequestsScreen). Должность НЕ вводится — берётся из справочника
|
*
|
||||||
* сотрудников (api.tnsu.kz/Employee/All, см. Employee.position в
|
* Одна форма на оба вида правки намеренно — раньше SLA жил в отдельной модалке
|
||||||
* models/dictionaries.ts) на момент сохранения, отдельного поля для неё
|
* со своей кнопкой-флажком в строке, и получалось два похожих диалога и лишняя
|
||||||
* в этой форме нет. Добавление НОВОГО шага через модалку не идёт — см.
|
* иконка в и без того плотном ряду действий. Правки применяются одним
|
||||||
|
* сохранением (см. saveModal в ManageRequestsScreen).
|
||||||
|
*
|
||||||
|
* Должность НЕ вводится — берётся из справочника сотрудников
|
||||||
|
* (api.tnsu.kz/Employee/All, см. Employee.position в models/dictionaries.ts) на
|
||||||
|
* момент сохранения. Добавление НОВОГО шага через модалку не идёт — см.
|
||||||
* плашку-пикер под таблицей в ManageRequestsPhaseTable.
|
* плашку-пикер под таблицей в ManageRequestsPhaseTable.
|
||||||
*/
|
*/
|
||||||
export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalProps> = ({
|
export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalProps> = ({
|
||||||
title,
|
title,
|
||||||
initialEmail = '',
|
initialEmail = '',
|
||||||
|
initialSlaHours,
|
||||||
|
initialAskOverdueReason,
|
||||||
employees,
|
employees,
|
||||||
employeesLoading,
|
employeesLoading,
|
||||||
excludedEmails,
|
excludedEmails,
|
||||||
@@ -34,6 +63,10 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
|||||||
onClose
|
onClose
|
||||||
}) => {
|
}) => {
|
||||||
const [email, setEmail] = React.useState(initialEmail);
|
const [email, setEmail] = React.useState(initialEmail);
|
||||||
|
const [slaHours, setSlaHours] = React.useState(() => slaHoursToInput(initialSlaHours));
|
||||||
|
const [askOverdueReason, setAskOverdueReason] = React.useState<TriState>(
|
||||||
|
() => boolToTriState(initialAskOverdueReason)
|
||||||
|
);
|
||||||
const submit = useAsyncSubmit();
|
const submit = useAsyncSubmit();
|
||||||
|
|
||||||
// Тот, кто уже занят в этой фазе, исключён из списка выбора — дубликат
|
// Тот, кто уже занят в этой фазе, исключён из списка выбора — дубликат
|
||||||
@@ -51,8 +84,14 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
|||||||
submit.fail(texts.manageRequests.validationDuplicateApprover);
|
submit.fail(texts.manageRequests.validationDuplicateApprover);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isValidSlaHours(slaHours)) {
|
||||||
|
submit.fail(texts.manageRequests.validationSlaHours);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const position = findEmployeeByEmail(employees, trimmedEmail)?.position || '';
|
const position = findEmployeeByEmail(employees, trimmedEmail)?.position || '';
|
||||||
submit.run(() => onSave(trimmedEmail, position));
|
submit.run(() =>
|
||||||
|
onSave(trimmedEmail, position, parseSlaHours(slaHours), triStateToBool(askOverdueReason))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -69,6 +108,22 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
|||||||
onChange={setEmail}
|
onChange={setEmail}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</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>
|
</FormGrid>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-17
@@ -38,7 +38,6 @@ export interface IManageRequestsPhaseTableProps {
|
|||||||
requestRejected: boolean;
|
requestRejected: boolean;
|
||||||
statusVariant: (status: string) => BadgeVariant;
|
statusVariant: (status: string) => BadgeVariant;
|
||||||
onReassign: (row: ManagedApprovalRow) => void;
|
onReassign: (row: ManagedApprovalRow) => void;
|
||||||
onEditSla: (row: ManagedApprovalRow) => void;
|
|
||||||
onRemove: (row: ManagedApprovalRow) => void;
|
onRemove: (row: ManagedApprovalRow) => void;
|
||||||
onNotify: (row: ManagedApprovalRow) => void;
|
onNotify: (row: ManagedApprovalRow) => void;
|
||||||
/** Id строки, для которой сейчас в процессе отправка уведомления — кнопка на ней отключена. */
|
/** Id строки, для которой сейчас в процессе отправка уведомления — кнопка на ней отключена. */
|
||||||
@@ -95,7 +94,6 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
|||||||
requestRejected,
|
requestRejected,
|
||||||
statusVariant,
|
statusVariant,
|
||||||
onReassign,
|
onReassign,
|
||||||
onEditSla,
|
|
||||||
onRemove,
|
onRemove,
|
||||||
onNotify,
|
onNotify,
|
||||||
notifyingRowId,
|
notifyingRowId,
|
||||||
@@ -269,16 +267,8 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
|||||||
const overdue =
|
const overdue =
|
||||||
(r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution) &&
|
(r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution) &&
|
||||||
isRowOverdue(r, allRows, requestCreatedIso, requestTypeSlaHours, new Date());
|
isRowOverdue(r, allRows, requestCreatedIso, requestTypeSlaHours, new Date());
|
||||||
const editable = !readOnly && rowEditable && !isGhost;
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td className={overdue ? styles.slaOverdue : undefined}>
|
||||||
className={[
|
|
||||||
overdue ? styles.slaOverdue : '',
|
|
||||||
editable ? styles.slaEditable : ''
|
|
||||||
].filter(Boolean).join(' ')}
|
|
||||||
title={editable ? texts.manageRequests.editSla : undefined}
|
|
||||||
onClick={editable ? () => onEditSla(r) : undefined}
|
|
||||||
>
|
|
||||||
{formatSlaHours(hours)}
|
{formatSlaHours(hours)}
|
||||||
{r.slaHours === null && hours !== null && (
|
{r.slaHours === null && hours !== null && (
|
||||||
<CellNote>{texts.manageRequests.slaInherited}</CellNote>
|
<CellNote>{texts.manageRequests.slaInherited}</CellNote>
|
||||||
@@ -335,12 +325,6 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
|||||||
label={texts.manageRequests.reassign}
|
label={texts.manageRequests.reassign}
|
||||||
onClick={() => onReassign(r)}
|
onClick={() => onReassign(r)}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
|
||||||
icon="flag"
|
|
||||||
className={styles.iconBtnVisible}
|
|
||||||
label={texts.manageRequests.editSla}
|
|
||||||
onClick={() => onEditSla(r)}
|
|
||||||
/>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="trash"
|
icon="trash"
|
||||||
className={styles.iconBtnVisible}
|
className={styles.iconBtnVisible}
|
||||||
|
|||||||
-12
@@ -105,15 +105,3 @@
|
|||||||
color: $red-500;
|
color: $red-500;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ячейка SLA, по которой можно кликнуть и открыть редактирование (см. onEditSla
|
|
||||||
// в ManageRequestsPhaseTable) — курсор и подчёркивание по пунктиру как явный
|
|
||||||
// намёк на кликабельность, без которого колонка неотличима от обычного текста.
|
|
||||||
.slaEditable {
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px dashed $gray-400;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $gray-50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import * as React from 'react';
|
|||||||
import styles from './ManageRequestsScreen.module.scss';
|
import styles from './ManageRequestsScreen.module.scss';
|
||||||
import { ManageRequestsApproverModal } from './ManageRequestsApproverModal';
|
import { ManageRequestsApproverModal } from './ManageRequestsApproverModal';
|
||||||
import { ManageRequestsPhaseTable } from './ManageRequestsPhaseTable';
|
import { ManageRequestsPhaseTable } from './ManageRequestsPhaseTable';
|
||||||
import { ManageRequestsSlaModal } from './ManageRequestsSlaModal';
|
|
||||||
import { ApprovalRouteApi } from '../../services/api';
|
import { ApprovalRouteApi } from '../../services/api';
|
||||||
import { PortalDictionaryClient } from '../../services/portalDictionary';
|
import { PortalDictionaryClient } from '../../services/portalDictionary';
|
||||||
import { RequestManageService } from '../../services/requestManageService';
|
import { RequestManageService } from '../../services/requestManageService';
|
||||||
@@ -179,7 +178,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
const [matrixLoading, setMatrixLoading] = React.useState(false);
|
const [matrixLoading, setMatrixLoading] = React.useState(false);
|
||||||
const [matrixError, setMatrixError] = React.useState<string | undefined>();
|
const [matrixError, setMatrixError] = React.useState<string | undefined>();
|
||||||
const [modal, setModal] = React.useState<ManagedApprovalRow | undefined>();
|
const [modal, setModal] = React.useState<ManagedApprovalRow | undefined>();
|
||||||
const [slaRow, setSlaRow] = React.useState<ManagedApprovalRow | undefined>();
|
|
||||||
const [deleteRow, setDeleteRow] = React.useState<ManagedApprovalRow | undefined>();
|
const [deleteRow, setDeleteRow] = React.useState<ManagedApprovalRow | undefined>();
|
||||||
const [saveError, setSaveError] = React.useState<string | undefined>();
|
const [saveError, setSaveError] = React.useState<string | undefined>();
|
||||||
const [notifyResult, setNotifyResult] = React.useState<{ message: string; kind: 'ok' | 'err' } | undefined>();
|
const [notifyResult, setNotifyResult] = React.useState<{ message: string; kind: 'ok' | 'err' } | undefined>();
|
||||||
@@ -350,31 +348,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
|
|
||||||
const openReassign = (row: ManagedApprovalRow): void => setModal(row);
|
const openReassign = (row: ManagedApprovalRow): void => setModal(row);
|
||||||
const closeModal = (): void => setModal(undefined);
|
const closeModal = (): void => setModal(undefined);
|
||||||
const openSla = (row: ManagedApprovalRow): void => setSlaRow(row);
|
|
||||||
const closeSla = (): void => setSlaRow(undefined);
|
|
||||||
|
|
||||||
// SLA правится так же оптимистично, как переназначение: строка в матрице
|
|
||||||
// обновляется по ответу сервера, без перезагрузки всей матрицы.
|
|
||||||
const saveSla = (
|
|
||||||
// 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 || !slaRow) return Promise.resolve();
|
|
||||||
setSaveError(undefined);
|
|
||||||
const rowId = slaRow.id;
|
|
||||||
return requestManage
|
|
||||||
.updateRowSla(matrix, rowId, slaHours, askOverdueReason)
|
|
||||||
.then(() => {
|
|
||||||
setMatrix(prev => (prev ? patchRow(prev, rowId, { slaHours, askOverdueReason }) : prev));
|
|
||||||
closeSla();
|
|
||||||
})
|
|
||||||
.catch((e: Error) => {
|
|
||||||
setSaveError(reportError(e, 'saveSla: updateRowSla'));
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Кого нельзя выбрать при переназначении — все остальные согласующие/
|
// Кого нельзя выбрать при переназначении — все остальные согласующие/
|
||||||
// исполнители ТОЙ ЖЕ фазы (сама строка modal исключена, иначе нельзя было
|
// исполнители ТОЙ ЖЕ фазы (сама строка modal исключена, иначе нельзя было
|
||||||
@@ -393,21 +366,40 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
// загруженного справочника сотрудников (тот же источник, что подставляет
|
// загруженного справочника сотрудников (тот же источник, что подставляет
|
||||||
// должность, — см. ManageRequestsApproverModal), чтобы не делать лишний
|
// должность, — см. 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();
|
if (!matrix || !modal) return Promise.resolve();
|
||||||
setSaveError(undefined);
|
setSaveError(undefined);
|
||||||
const rowId = modal.id;
|
const rowId = modal.id;
|
||||||
|
// Два запроса последовательно, а не параллельно: оба правят ОДНУ строку
|
||||||
|
// SharePoint, и параллельные update по одному элементу списка могут
|
||||||
|
// затереть друг друга (последний выигрывает).
|
||||||
return requestManage
|
return requestManage
|
||||||
.reassignApprover(matrix, rowId, email, position)
|
.reassignApprover(matrix, rowId, email, position)
|
||||||
|
.then(() => requestManage.updateRowSla(matrix, rowId, slaHours, askOverdueReason))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const found = findEmployeeByEmail(employees.items, email);
|
const found = findEmployeeByEmail(employees.items, email);
|
||||||
setMatrix(prev =>
|
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();
|
closeModal();
|
||||||
})
|
})
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
setSaveError(reportError(e, 'saveModal: reassignApprover'));
|
setSaveError(reportError(e, 'saveModal: reassignApprover/updateRowSla'));
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -583,7 +575,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
requestRejected={requestRejected}
|
requestRejected={requestRejected}
|
||||||
statusVariant={statusVariant}
|
statusVariant={statusVariant}
|
||||||
onReassign={openReassign}
|
onReassign={openReassign}
|
||||||
onEditSla={openSla}
|
|
||||||
onRemove={askDelete}
|
onRemove={askDelete}
|
||||||
onNotify={handleNotify}
|
onNotify={handleNotify}
|
||||||
notifyingRowId={notifyingRowId}
|
notifyingRowId={notifyingRowId}
|
||||||
@@ -603,7 +594,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
requestRejected={requestRejected}
|
requestRejected={requestRejected}
|
||||||
statusVariant={statusVariant}
|
statusVariant={statusVariant}
|
||||||
onReassign={openReassign}
|
onReassign={openReassign}
|
||||||
onEditSla={openSla}
|
|
||||||
onRemove={askDelete}
|
onRemove={askDelete}
|
||||||
onNotify={handleNotify}
|
onNotify={handleNotify}
|
||||||
notifyingRowId={notifyingRowId}
|
notifyingRowId={notifyingRowId}
|
||||||
@@ -622,6 +612,8 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
<ManageRequestsApproverModal
|
<ManageRequestsApproverModal
|
||||||
title={texts.manageRequests.reassignTitle}
|
title={texts.manageRequests.reassignTitle}
|
||||||
initialEmail={modal.approverEmail}
|
initialEmail={modal.approverEmail}
|
||||||
|
initialSlaHours={modal.slaHours}
|
||||||
|
initialAskOverdueReason={modal.askOverdueReason}
|
||||||
employees={employees.items}
|
employees={employees.items}
|
||||||
employeesLoading={employees.isLoading}
|
employeesLoading={employees.isLoading}
|
||||||
excludedEmails={reassignExcludedEmails}
|
excludedEmails={reassignExcludedEmails}
|
||||||
@@ -630,16 +622,6 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{slaRow && (
|
|
||||||
<ManageRequestsSlaModal
|
|
||||||
orderNo={slaRow.order}
|
|
||||||
slaHours={slaRow.slaHours}
|
|
||||||
askOverdueReason={slaRow.askOverdueReason}
|
|
||||||
onSave={saveSla}
|
|
||||||
onClose={closeSla}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deleteRow && (
|
{deleteRow && (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title={texts.manageRequests.deleteTitle}
|
title={texts.manageRequests.deleteTitle}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { texts } from '../../texts';
|
|
||||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
|
||||||
import {
|
|
||||||
boolToTriState,
|
|
||||||
isValidSlaHours,
|
|
||||||
parseSlaHours,
|
|
||||||
slaHoursToInput,
|
|
||||||
triStateToBool,
|
|
||||||
TriState,
|
|
||||||
TRI_STATE_OPTIONS
|
|
||||||
} from '../../utils/sla';
|
|
||||||
import { Field, FormGrid, FormModal, Select, TextInput } from '../ui';
|
|
||||||
|
|
||||||
export interface IManageRequestsSlaModalProps {
|
|
||||||
/** Номер шага — только для заголовка, чтобы было видно, что правим. */
|
|
||||||
orderNo: number;
|
|
||||||
// eslint-disable-next-line @rushstack/no-new-null -- null = «в строке не задано», это не 0
|
|
||||||
slaHours: number | null;
|
|
||||||
// eslint-disable-next-line @rushstack/no-new-null -- null = «в строке не задано»
|
|
||||||
askOverdueReason: boolean | null;
|
|
||||||
// eslint-disable-next-line @rushstack/no-new-null -- см. выше
|
|
||||||
onSave: (slaHours: number | null, askOverdueReason: boolean | null) => Promise<void>;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SLA конкретной строки маршрута конкретной заявки.
|
|
||||||
*
|
|
||||||
* Правится отдельно от маршрута-шаблона: шаблон задаёт значение на момент
|
|
||||||
* создания заявки, а здесь его можно поменять для одной уже идущей заявки, не
|
|
||||||
* трогая остальные.
|
|
||||||
*
|
|
||||||
* Пустое поле сохраняется как «не задано» — тогда работает SLA типа заявки.
|
|
||||||
* Ноль пустотой не считается: это «напоминать сразу».
|
|
||||||
*/
|
|
||||||
export const ManageRequestsSlaModal: React.FC<IManageRequestsSlaModalProps> = ({
|
|
||||||
orderNo,
|
|
||||||
slaHours,
|
|
||||||
askOverdueReason,
|
|
||||||
onSave,
|
|
||||||
onClose
|
|
||||||
}) => {
|
|
||||||
const [hours, setHours] = React.useState(() => slaHoursToInput(slaHours));
|
|
||||||
const [ask, setAsk] = React.useState<TriState>(() => boolToTriState(askOverdueReason));
|
|
||||||
const submit = useAsyncSubmit();
|
|
||||||
|
|
||||||
const handleSubmit = (): void => {
|
|
||||||
if (!isValidSlaHours(hours)) {
|
|
||||||
submit.fail(texts.manageRequests.validationSlaHours);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
submit.run(() => onSave(parseSlaHours(hours), triStateToBool(ask)));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormModal
|
|
||||||
title={texts.manageRequests.slaModalTitle(orderNo)}
|
|
||||||
error={submit.error}
|
|
||||||
isSaving={submit.isBusy}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<FormGrid>
|
|
||||||
<Field label={texts.manageRequests.slaHoursLabel} full hint={texts.manageRequests.slaHoursHint}>
|
|
||||||
<TextInput
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
value={hours}
|
|
||||||
onChange={e => setHours(e.target.value)}
|
|
||||||
placeholder={texts.manageRequests.slaHoursPlaceholder}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label={texts.manageRequests.askOverdueReasonLabel} full hint={texts.manageRequests.askOverdueReasonHint}>
|
|
||||||
<Select
|
|
||||||
options={TRI_STATE_OPTIONS}
|
|
||||||
value={ask}
|
|
||||||
onChange={e => setAsk(e.target.value as TriState)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</FormGrid>
|
|
||||||
</FormModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -342,8 +342,6 @@ export const texts = {
|
|||||||
reassignTitle: 'Переназначить шаг',
|
reassignTitle: 'Переназначить шаг',
|
||||||
columnSla: 'SLA',
|
columnSla: 'SLA',
|
||||||
slaInherited: 'от типа заявки',
|
slaInherited: 'от типа заявки',
|
||||||
editSla: 'Настроить SLA шага',
|
|
||||||
slaModalTitle: (orderNo: number): string => `SLA шага ${orderNo}`,
|
|
||||||
slaHoursLabel: 'SLA, часов',
|
slaHoursLabel: 'SLA, часов',
|
||||||
slaHoursHint:
|
slaHoursHint:
|
||||||
'Через сколько часов простоя напоминать по этому шагу. Пусто — берётся SLA типа заявки; 0 — напоминать сразу.',
|
'Через сколько часов простоя напоминать по этому шагу. Пусто — берётся SLA типа заявки; 0 — напоминать сразу.',
|
||||||
|
|||||||
Reference in New Issue
Block a user