172 lines
6.1 KiB
TypeScript
172 lines
6.1 KiB
TypeScript
import * as React from 'react';
|
|
import { Company, Department, RequestType, TemplateRoute } from '../../models';
|
|
import { texts } from '../../texts';
|
|
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
|
import { ApprovalRouteApi } from '../../services/api';
|
|
import { Checkbox, Field, FormGrid, FormModal, SearchableSelect, Select } from '../ui';
|
|
|
|
export interface IRouteFormModalProps {
|
|
api: ApprovalRouteApi;
|
|
/** Undefined — создание нового маршрута, иначе редактирование существующего. */
|
|
editing?: TemplateRoute;
|
|
/** Тип заявки, под которым открыли форму создания (например, из активного фильтра списка). */
|
|
initialRequestTypeId: string;
|
|
requestTypes: RequestType[];
|
|
companies: Company[];
|
|
departments: Department[];
|
|
dictionaryLoading: boolean;
|
|
onSaved: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
interface IFormState {
|
|
requestTypeId: string;
|
|
companyId: string;
|
|
departmentId: string;
|
|
byProject: boolean;
|
|
requiresControl: boolean;
|
|
}
|
|
|
|
function toFormState(editing: TemplateRoute | undefined, initialRequestTypeId: string): IFormState {
|
|
if (!editing) {
|
|
return {
|
|
requestTypeId: initialRequestTypeId,
|
|
companyId: '',
|
|
departmentId: '',
|
|
byProject: false,
|
|
requiresControl: false
|
|
};
|
|
}
|
|
return {
|
|
requestTypeId: String(editing.requestTypeId),
|
|
companyId: editing.companyId ?? '',
|
|
departmentId: editing.departmentId ?? '',
|
|
byProject: editing.byProject,
|
|
requiresControl: editing.requiresControl
|
|
};
|
|
}
|
|
|
|
/** Форма создания/редактирования маршрута — вынесена из RoutesScreen, чтобы тот не разрастался. */
|
|
export const RouteFormModal: React.FC<IRouteFormModalProps> = ({
|
|
api,
|
|
editing,
|
|
initialRequestTypeId,
|
|
requestTypes,
|
|
companies,
|
|
departments,
|
|
dictionaryLoading,
|
|
onSaved,
|
|
onClose
|
|
}) => {
|
|
const [form, setForm] = React.useState<IFormState>(() => toFormState(editing, initialRequestTypeId));
|
|
const submit = useAsyncSubmit();
|
|
|
|
// Отдел принадлежит компании, поэтому список сужаем под выбранную:
|
|
// иначе пользователь листает ~700 позиций из всех юрлиц сразу.
|
|
const departmentsForSelectedCompany = React.useMemo(
|
|
() => (form.companyId ? departments.filter(d => d.companyId === form.companyId) : []),
|
|
[departments, form.companyId]
|
|
);
|
|
|
|
const submitForm = (): void => {
|
|
if (!editing && !form.requestTypeId) {
|
|
submit.fail(texts.routes.validationRequestType);
|
|
return;
|
|
}
|
|
|
|
const companyId = form.companyId.trim() ? form.companyId.trim() : null;
|
|
const departmentId = form.departmentId.trim() ? form.departmentId.trim() : null;
|
|
const request = editing
|
|
? api.templateRoutes.update(editing.id, {
|
|
companyId,
|
|
departmentId,
|
|
byProject: form.byProject,
|
|
requiresControl: form.requiresControl
|
|
})
|
|
: api.templateRoutes.create({
|
|
requestTypeId: Number(form.requestTypeId),
|
|
companyId,
|
|
departmentId,
|
|
byProject: form.byProject,
|
|
requiresControl: form.requiresControl
|
|
});
|
|
|
|
submit.run(() =>
|
|
request.then(() => {
|
|
onSaved();
|
|
onClose();
|
|
})
|
|
);
|
|
};
|
|
|
|
return (
|
|
<FormModal
|
|
title={editing ? texts.routes.editTitle(editing.id) : texts.routes.createTitle}
|
|
error={submit.error}
|
|
isSaving={submit.isBusy}
|
|
onSubmit={submitForm}
|
|
onClose={onClose}
|
|
>
|
|
<FormGrid>
|
|
<Field label={texts.routes.requestTypeLabel} required full>
|
|
<Select
|
|
options={requestTypes.map(rt => ({ value: String(rt.id), label: rt.name }))}
|
|
placeholder={texts.routes.requestTypePlaceholder}
|
|
value={form.requestTypeId}
|
|
disabled={!!editing}
|
|
onChange={e => setForm({ ...form, requestTypeId: e.target.value })}
|
|
/>
|
|
</Field>
|
|
<Field label={texts.routes.companyLabel} full hint={texts.routes.companyHint}>
|
|
<SearchableSelect
|
|
options={companies.map(c => ({ value: c.id, label: c.name }))}
|
|
value={form.companyId}
|
|
loading={dictionaryLoading}
|
|
allowClear
|
|
clearLabel={texts.routes.companyClear}
|
|
placeholder={texts.routes.companyPlaceholder}
|
|
searchPlaceholder={texts.routes.companySearchPlaceholder}
|
|
onChange={companyId =>
|
|
// Смена компании обнуляет отдел: прежний принадлежал другой
|
|
// компании, и бэкенд не нашёл бы такой маршрут при resolve.
|
|
setForm({ ...form, companyId, departmentId: '' })
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label={texts.routes.departmentLabel}
|
|
full
|
|
hint={form.companyId ? texts.routes.departmentHint : texts.routes.departmentHintNoCompany}
|
|
>
|
|
<SearchableSelect
|
|
options={departmentsForSelectedCompany.map(d => ({ value: d.id, label: d.name }))}
|
|
value={form.departmentId}
|
|
loading={dictionaryLoading}
|
|
disabled={!form.companyId}
|
|
allowClear
|
|
clearLabel={texts.routes.departmentPlaceholder}
|
|
placeholder={texts.routes.departmentPlaceholder}
|
|
searchPlaceholder={texts.routes.departmentSearchPlaceholder}
|
|
emptyMessage={texts.routes.departmentEmpty}
|
|
onChange={departmentId => setForm({ ...form, departmentId })}
|
|
/>
|
|
</Field>
|
|
<Field label={texts.routes.byProjectLabel}>
|
|
<Checkbox
|
|
label={texts.routes.byProjectCheckbox}
|
|
checked={form.byProject}
|
|
onChange={v => setForm({ ...form, byProject: v })}
|
|
/>
|
|
</Field>
|
|
<Field label={texts.routes.controlLabel}>
|
|
<Checkbox
|
|
label={texts.routes.controlCheckbox}
|
|
checked={form.requiresControl}
|
|
onChange={v => setForm({ ...form, requiresControl: v })}
|
|
/>
|
|
</Field>
|
|
</FormGrid>
|
|
</FormModal>
|
|
);
|
|
};
|