Фронтенд для сервиса (#5)
Tests / test (push) Successful in 2m26s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/5
This commit was merged in pull request #5.
This commit is contained in:
2026-07-30 06:11:24 +00:00
parent 5476c1bab5
commit da158cfd84
114 changed files with 26045 additions and 2 deletions
@@ -0,0 +1,223 @@
import * as React from 'react';
import {
Employee,
HeadType,
ProjectRole,
RouteStepStatus,
StepExecutorKind,
TemplateRoute,
TemplateRouteStep
} from '../../models';
import { headTypeLabels, projectRoleLabels, routeStepStatusLabels, texts } from '../../texts';
import { detectExecutorKind } from '../../utils/stepExecutor';
import { toEmployeeOption } from '../../utils/employee';
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
import { ApprovalRouteApi } from '../../services/api';
import { Checkbox, Field, FormGrid, FormModal, RadioGroup, SearchableSelect, Select, TextInput } from '../ui';
export interface IRouteStepFormModalProps {
api: ApprovalRouteApi;
route: TemplateRoute;
/** 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;
}
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] }));
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
};
}
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
};
}
/** Форма создания/редактирования шага маршрута — вынесена из RouteStepsScreen. */
export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
api,
route,
editing,
nextOrderNo,
employees,
employeesLoading,
onSaved,
onClose
}) => {
const [form, setForm] = React.useState<IFormState>(() => toFormState(editing, nextOrderNo));
const submit = useAsyncSubmit();
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;
}
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
};
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 (
<FormModal
title={editing ? texts.routeSteps.editTitle(editing.orderNo) : texts.routeSteps.createTitle}
error={submit.error}
isSaving={submit.isBusy}
onSubmit={submitForm}
onClose={onClose}
>
<FormGrid>
<Field label={texts.routeSteps.orderNoLabel} required>
<TextInput
type="number"
min={1}
value={form.orderNo}
onChange={e => setForm({ ...form, orderNo: e.target.value })}
/>
</Field>
<Field label={texts.routeSteps.statusLabel} required>
<Select
options={STATUS_OPTIONS}
value={form.status}
onChange={e => setForm({ ...form, status: e.target.value as RouteStepStatus })}
/>
</Field>
<Field label={texts.routeSteps.kindLabel} required full hint={texts.routeSteps.kindHint}>
<RadioGroup
name="executorKind"
value={form.kind}
onChange={kind => setForm({ ...form, kind })}
options={[
{ value: 'email', label: texts.routeSteps.kindEmail },
{ value: 'position', label: texts.routeSteps.kindPosition },
{
value: 'projectRole',
label: texts.routeSteps.kindProjectRole,
disabled: !route.byProject,
disabledHint: texts.routeSteps.kindProjectRoleDisabled
},
{ value: 'headType', label: texts.routeSteps.kindHeadType }
]}
/>
</Field>
{form.kind === 'email' && (
<Field label={texts.routeSteps.executorLabel} required full>
<SearchableSelect
options={employees.map(toEmployeeOption)}
value={form.executorEmail}
loading={employeesLoading}
placeholder={texts.routeSteps.executorPlaceholder}
searchPlaceholder={texts.select.employeeSearchPlaceholder}
emptyMessage={texts.select.employeesEmpty}
onChange={executorEmail => setForm({ ...form, executorEmail })}
/>
</Field>
)}
{form.kind === 'position' && (
<Field label={texts.routeSteps.positionLabel} required full hint={texts.routeSteps.positionHint}>
<TextInput
value={form.position}
onChange={e => setForm({ ...form, position: e.target.value })}
placeholder={texts.routeSteps.positionPlaceholder}
/>
</Field>
)}
{form.kind === 'projectRole' && (
<Field label={texts.routeSteps.projectRoleLabel} required full>
<Select
options={PROJECT_ROLE_OPTIONS}
value={form.projectRole}
onChange={e => setForm({ ...form, projectRole: e.target.value as ProjectRole })}
/>
</Field>
)}
{form.kind === 'headType' && (
<Field label={texts.routeSteps.headTypeLabel} required full>
<Select
options={HEAD_TYPE_OPTIONS}
value={form.headType}
onChange={e => setForm({ ...form, headType: e.target.value as HeadType })}
/>
</Field>
)}
<Field label={texts.routeSteps.reassignLabel} full>
<Checkbox
label={texts.routeSteps.reassignCheckbox}
checked={form.allowReassignToOtherApprovers}
onChange={v => setForm({ ...form, allowReassignToOtherApprovers: v })}
/>
</Field>
</FormGrid>
</FormModal>
);
};