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)} />