import * as React from 'react'; import { ComparisonOperator, Employee, HeadType, ProjectRole, RequestType, RouteStepStatus, StepExecutorKind, TemplateRoute, TemplateRouteStep } from '../../models'; import { comparisonOperatorLabels, headTypeLabels, projectRoleLabels, routeStepStatusLabels, texts } from '../../texts'; import { detectExecutorKind } from '../../utils/stepExecutor'; import { toEmployeeOption } from '../../utils/employee'; import { useAsyncSubmit } from '../../hooks/useAsyncSubmit'; 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'; export interface IRouteStepFormModalProps { api: ApprovalRouteApi; route: TemplateRoute; /** Undefined, пока грузится тип заявки маршрута. */ requestType?: RequestType; sharePoint: SharePointListsClient; /** Undefined — создание нового шага, иначе редактирование существующего. */ editing?: TemplateRouteStep; nextOrderNo: number; employees: Employee[]; employeesLoading: boolean; onSaved: (savedStep: TemplateRouteStep) => void; onClose: () => void; } interface IFormState { orderNo: string; status: RouteStepStatus; kind: StepExecutorKind; executorEmail: string; position: string; projectRole: ProjectRole; headType: HeadType; allowReassignToOtherApprovers: boolean; hasCondition: boolean; fieldName: string; operation: ComparisonOperator; value: string; } const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution] .map(v => ({ value: v, label: routeStepStatusLabels[v] })); const PROJECT_ROLE_OPTIONS = Object.values(ProjectRole) .filter(v => v !== ProjectRole.None) .map(v => ({ value: v, label: projectRoleLabels[v] })); const HEAD_TYPE_OPTIONS = Object.values(HeadType) .filter(v => v !== HeadType.None) .map(v => ({ value: v, label: headTypeLabels[v] })); const OPERATION_OPTIONS = Object.values(ComparisonOperator) .filter(v => v !== ComparisonOperator.None) .map(v => ({ value: v, label: comparisonOperatorLabels[v] })); function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number): IFormState { if (!editing) { return { orderNo: String(nextOrderNo), status: RouteStepStatus.Approval, kind: 'email', executorEmail: '', position: '', projectRole: ProjectRole.ProjectManager, headType: HeadType.Chief, allowReassignToOtherApprovers: false, hasCondition: false, fieldName: '', operation: ComparisonOperator.GreaterThan, value: '' }; } return { orderNo: String(editing.orderNo), status: editing.status, kind: detectExecutorKind(editing), executorEmail: editing.executorEmail ?? '', position: editing.position ?? '', projectRole: editing.projectRole && editing.projectRole !== ProjectRole.None ? editing.projectRole : ProjectRole.ProjectManager, headType: editing.headType && editing.headType !== HeadType.None ? editing.headType : HeadType.Chief, allowReassignToOtherApprovers: editing.allowReassignToOtherApprovers, hasCondition: Boolean(editing.fieldName), fieldName: editing.fieldName ?? '', operation: editing.operation && editing.operation !== ComparisonOperator.None ? editing.operation : ComparisonOperator.GreaterThan, value: editing.value ?? '' }; } /** Форма создания/редактирования шага маршрута — вынесена из RouteStepsScreen. */ export const RouteStepFormModal: React.FC = ({ api, route, requestType, sharePoint, editing, nextOrderNo, employees, employeesLoading, onSaved, onClose }) => { const [form, setForm] = React.useState(() => toFormState(editing, nextOrderNo)); const submit = useAsyncSubmit(); const hasListName = Boolean(requestType?.listName); const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl, requestType?.listName); const fieldNameOptions = React.useMemo(() => { const options = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title })); if (form.fieldName && !options.some(o => o.value === form.fieldName)) { options.unshift({ value: form.fieldName, label: form.fieldName }); } return options; }, [sharePointFields.items, form.fieldName]); // Тип столбца известен, только если он есть среди подгруженных полей списка — // для значения, введённого до появления поля в списке (или после его удаления), // ограничения ниже не действуют, чтобы не блокировать уже сохранённые данные. const selectedField = sharePointFields.items.find(f => f.internalName === form.fieldName); const isNumericField = selectedField ? isNumericFieldType(selectedField.typeAsString) : true; const operationOptions = isNumericField ? OPERATION_OPTIONS : OPERATION_OPTIONS.filter(o => o.value === ComparisonOperator.Equal); // Строковая колонка допускает только «равно» — если пользователь сменил // field_name на нечисловую колонку, а раньше был выбран «больше»/«меньше», сбрасываем. React.useEffect(() => { if (!isNumericField && form.operation !== ComparisonOperator.Equal) { setForm(f => ({ ...f, operation: ComparisonOperator.Equal })); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isNumericField]); const submitForm = (): void => { const orderNo = Number(form.orderNo); if (!orderNo || orderNo < 1) { submit.fail(texts.routeSteps.validationOrderNo); return; } if (form.kind === 'email' && !form.executorEmail.trim()) { submit.fail(texts.routeSteps.validationExecutor); return; } if (form.kind === 'position' && !form.position.trim()) { submit.fail(texts.routeSteps.validationPosition); return; } if (form.hasCondition && (!form.fieldName.trim() || !form.value.trim())) { submit.fail(texts.routeSteps.validationCondition); return; } if (form.hasCondition && isNumericField && !Number.isFinite(Number(form.value.trim()))) { submit.fail(texts.routeSteps.validationConditionNumber); return; } const payload = { orderNo, status: form.status, allowReassignToOtherApprovers: form.allowReassignToOtherApprovers, executorEmail: form.kind === 'email' ? form.executorEmail.trim() : null, position: form.kind === 'position' ? form.position.trim() : null, projectRole: form.kind === 'projectRole' ? form.projectRole : null, 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 }; const request = editing ? api.templateRouteSteps.update(route.id, editing.id, payload) : api.templateRouteSteps.create(route.id, payload); submit.run(() => request.then(step => { onSaved(step); onClose(); }) ); }; return ( setForm({ ...form, orderNo: e.target.value })} /> setForm({ ...form, projectRole: e.target.value as ProjectRole })} /> )} {form.kind === 'headType' && ( setForm({ ...form, fieldName: e.target.value })} />