fix: заявки рекрутинга не давали отправить без выьбранного проекта, но пропускали пустую матрицу согласования
Tests / frontend-test (push) Successful in 1m23s

This commit is contained in:
Тимур Городецкий
2026-09-21 09:29:34 +05:00
parent 070c3ad752
commit 25c1188c32
11 changed files with 342 additions and 10 deletions
@@ -196,10 +196,20 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
// При совмещении (2+ компании в «Организация») нужно явно указать, какая из них основная.
const mainCompanyMissing = data.Companies.length >= 2 && !data.MainCompanyId;
const isValid = data.HireCandidateName.trim().length > 0 && data.Position.trim().length > 0 && !mainCompanyMissing;
// ProjectId обязателен (а не просто текст Project): маршрут резолвится ПО
// ПРОЕКТУ (ByProject, см. projectId выше), и без выбранного из списка
// варианта (id) approval-route вернёт 404 — форма должна не пускать на
// отправку раньше, чем выяснится, что маршрут вообще некому строить (баг:
// раньше это не проверялось, и заявка молча уходила на согласование с
// пустой матрицей).
const isValid =
data.HireCandidateName.trim().length > 0 &&
data.Position.trim().length > 0 &&
!!data.ProjectId &&
!mainCompanyMissing;
const validate = (): string | undefined => {
if (mainCompanyMissing) return 'Выбрано несколько организаций — укажите, какая из них основное место работы.';
if (!isValid) return 'Заполните обязательные поля: Ф.И.О. кандидата, Должность.';
if (!isValid) return 'Заполните обязательные поля: Ф.И.О. кандидата, Должность, Проект (выберите вариант из списка).';
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
return undefined;
@@ -296,6 +306,20 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
const validateSubmit = (): string | undefined => {
const formError = validate();
if (formError) return formError;
if (matrix.error) {
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
}
if (matrix.loading) {
return 'Маршрут согласования ещё формируется — подождите немного и повторите.';
}
// Матрица пуста (не только «есть шаг без человека») — раньше это молча
// пропускалось (findUnassignedStep на пустом массиве ошибки не даёт), и
// заявка уходила на согласование без единого согласующего. У фазы
// исполнения (executors) пустой список — нормальное состояние (не у
// всех маршрутов есть шаги исполнения), там эта проверка не нужна.
if (matrix.rows.length === 0) {
return 'Маршрут согласования не найден для выбранного проекта — обратитесь к администратору.';
}
const unassignedApproverError = unassignedStepMessage(
matrix.rows,
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
@@ -49,7 +49,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
shares={data.CompanyShares}
onSharesChange={(v) => set('CompanyShares', v)}
/>
<FormField label="1.2 Проект" htmlFor="Project" full>
<FormField label="1.2 Проект" htmlFor="Project" required full>
<ProjectPicker
context={context}
id="Project"
@@ -1,8 +1,9 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput } from '../../../shared/ui';
import { PositionPicker } from '../../../shared/dictionary';
import { ISectionProps } from '../model';
export const CandidateSection: React.FC<ISectionProps> = ({ data, set }) => (
export const CandidateSection: React.FC<ISectionProps> = ({ data, set, context }) => (
<CollapseSection title="2. Кандидат" icon="people">
<FormField label="2.1 Ф.И.О. кандидата" htmlFor="HireCandidateName" required full>
<TextInput id="HireCandidateName" value={data.HireCandidateName} onChange={(v) => set('HireCandidateName', v)} />
@@ -11,7 +12,13 @@ export const CandidateSection: React.FC<ISectionProps> = ({ data, set }) => (
<TextInput id="HireHireDate" type="date" value={data.HireHireDate} onChange={(v) => set('HireHireDate', v)} />
</FormField>
<FormField label="2.3 Должность" htmlFor="Position" required>
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
<PositionPicker
context={context}
id="Position"
value={data.Position}
onChange={(v) => set('Position', v)}
placeholder="Начните вводить название должности…"
/>
</FormField>
<FormField label="2.4 Вид работ" htmlFor="WorkKind">
<TextInput id="WorkKind" value={data.WorkKind} onChange={(v) => set('WorkKind', v)} />
@@ -207,9 +207,23 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
(login) => approval.getJobTitle(login)
);
const isValid = !!(data.Responsible && data.DepartmentName.trim() && data.Position.trim() && data.Residence);
// ObjectProjectId обязателен (а не просто текст ObjectProject): маршрут
// согласования резолвится ПО ПРОЕКТУ (ByProject, см. projectId выше), и без
// выбранного из списка варианта (id) approval-route вернёт 404 — форма
// должна не пускать на отправку раньше, чем выяснится, что маршрут вообще
// некому строить (баг: раньше это не проверялось, и заявка молча уходила
// на согласование с пустой матрицей).
const isValid = !!(
data.Responsible &&
data.DepartmentName.trim() &&
data.Position.trim() &&
data.Residence &&
data.ObjectProjectId
);
const validate = (): string | undefined => {
if (!isValid) return 'Заполните обязательные поля: ответственный за подачу, название отдела, должность, проживание.';
if (!isValid) {
return 'Заполните обязательные поля: ответственный за подачу, название отдела, должность, проживание, объект/проект (выберите вариант из списка).';
}
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
return undefined;
@@ -295,6 +309,15 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
if (matrix.error) {
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
}
if (matrix.loading) {
return 'Маршрут согласования ещё формируется — подождите немного и повторите.';
}
// Матрица пуста (не только «есть шаг без человека») — раньше это молча
// пропускалось (findUnassignedStep на пустом массиве ошибки не даёт), и
// заявка уходила на согласование без единого согласующего.
if (matrix.rows.length === 0) {
return 'Маршрут согласования не найден для выбранного объекта/проекта — обратитесь к администратору.';
}
const unassignedError = unassignedStepMessage(
matrix.rows,
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
@@ -76,7 +76,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
placeholder="Начните вводить название организации…"
/>
</FormField>
<FormField label="1.7 Объект/Проект" htmlFor="ObjectProject" full>
<FormField label="1.7 Объект/Проект" htmlFor="ObjectProject" required full>
<ProjectPicker
context={context}
id="ObjectProject"
@@ -1,5 +1,6 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea, Select } from '../../../shared/ui';
import { PositionPicker } from '../../../shared/dictionary';
import { LanguagesField } from './LanguagesField';
import { MultiChoiceField } from './MultiChoiceField';
import {
@@ -12,10 +13,16 @@ import {
OTHER_VALUE
} from '../model';
export const VacancySection: React.FC<ISectionProps> = ({ data, set }) => (
export const VacancySection: React.FC<ISectionProps> = ({ data, set, context }) => (
<CollapseSection title="3. Вакансия" icon="document">
<FormField label="3.1 Должность" htmlFor="Position" required full>
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
<PositionPicker
context={context}
id="Position"
value={data.Position}
onChange={(v) => set('Position', v)}
placeholder="Начните вводить название должности…"
/>
</FormField>
<FormField label="3.2 Характер занятости" htmlFor="EmploymentType">
<Select