refactor: причины открытия вакансии перенесены под количество вакансий (п. 2.3)
Tests / frontend-test (push) Successful in 1m16s

This commit is contained in:
Тимур Городецкий
2026-09-25 11:39:00 +05:00
parent 7959d91025
commit d539cdbfd2
7 changed files with 54 additions and 51 deletions
@@ -121,7 +121,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
}, [editId, editListId, notifyCode, setSubmitError]); }, [editId, editListId, notifyCode, setSubmitError]);
// Компании ответственного (Employee/ByEmail.company/companyId) → предзаполнение // Компании ответственного (Employee/ByEmail.company/companyId) → предзаполнение
// «1.6 Организация» (Companies/CompanyPickerMulti). companyId из Employee/ByEmail // «1.5 Организация» (Companies/CompanyPickerMulti). companyId из Employee/ByEmail
// совпадает с Id из Dictionary/Companies (тот же источник) — доп. запрос не нужен. // совпадает с Id из Dictionary/Companies (тот же источник) — доп. запрос не нужен.
// Поле остаётся редактируемым: значения только ПРЕДЛАГАЮТСЯ, как и DepartmentName. // Поле остаётся редактируемым: значения только ПРЕДЛАГАЮТСЯ, как и DepartmentName.
const toCompanies = (records: IEmployeeOrganization[]): ICompany[] => const toCompanies = (records: IEmployeeOrganization[]): ICompany[] =>
@@ -450,7 +450,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
editor={matrix} editor={matrix}
search={(q) => approval.searchUsers(q)} search={(q) => approval.searchUsers(q)}
waiting={!matrixReady} waiting={!matrixReady}
waitingText="Выберите «Объект/Проект» (п. 1.7), чтобы построить маршрут согласования." waitingText="Выберите «Объект/Проект» (п. 1.6), чтобы построить маршрут согласования."
canRemoveRow={canRemoveApprover ? () => true : undefined} canRemoveRow={canRemoveApprover ? () => true : undefined}
/> />
@@ -20,7 +20,7 @@ export interface IFormData {
SubmitDate: string; SubmitDate: string;
ClosingDeadline: string; ClosingDeadline: string;
/** /**
* 1.5 Причины открытия вакансии. Одна причина — при одной вакансии; при 2.2 > 1 * 2.3 Причины открытия вакансии. Одна причина — при одной вакансии; при 2.2 > 1
* можно несколько, и у причин с ФИО — несколько сотрудников. Хранится JSON в * можно несколько, и у причин с ФИО — несколько сотрудников. Хранится JSON в
* колонке VacancyReasonsJson; старые колонки VacancyOpenReason (первая * колонке VacancyReasonsJson; старые колонки VacancyOpenReason (первая
* причина) и VacancyOpenReasonDetails (сводка текстом) заполняются для * причина) и VacancyOpenReasonDetails (сводка текстом) заполняются для
@@ -83,7 +83,7 @@ export interface IFormData {
HiringDecisionMaker: string[]; HiringDecisionMaker: string[];
} }
/** Одна причина открытия вакансии (п. 1.5). */ /** Одна причина открытия вакансии (п. 2.3). */
export interface IVacancyReason { export interface IVacancyReason {
/** Значение из OPEN_REASON_OPTIONS. */ /** Значение из OPEN_REASON_OPTIONS. */
reason: string; reason: string;
@@ -2,12 +2,8 @@ import * as React from 'react';
import { CollapseSection, FormField, TextInput, PeopleField } from '../../../shared/ui'; import { CollapseSection, FormField, TextInput, PeopleField } from '../../../shared/ui';
import { CompanyPickerMulti, ProjectPicker } from '../../../shared/dictionary'; import { CompanyPickerMulti, ProjectPicker } from '../../../shared/dictionary';
import { ISectionProps, PERSON_PLACEHOLDER } from '../model'; import { ISectionProps, PERSON_PLACEHOLDER } from '../model';
import { parseVacanciesCount } from '../vacancyReasons';
import { VacancyReasonsField } from './VacancyReasonsField';
export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context, onResponsibleChange }) => { export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context, onResponsibleChange }) => {
const vacanciesCount = parseVacanciesCount(data.OpenVacanciesCount);
return ( return (
<CollapseSection title="1. Заявка" icon="clipboard"> <CollapseSection title="1. Заявка" icon="clipboard">
<FormField label="1.1 Ответственный за подачу заявки" fieldKey="Responsible" required full> <FormField label="1.1 Ответственный за подачу заявки" fieldKey="Responsible" required full>
@@ -38,19 +34,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
min={data.SubmitDate || undefined} min={data.SubmitDate || undefined}
/> />
</FormField> </FormField>
<FormField <FormField label="1.5 Организация" htmlFor="Companies" required full>
label={vacanciesCount > 1 ? '1.5 Причины открытия вакансий' : '1.5 Причина открытия вакансии'}
fieldKey="VacancyReasons"
required
full
>
<VacancyReasonsField
value={data.VacancyReasons}
onChange={(v) => set('VacancyReasons', v)}
vacanciesCount={vacanciesCount}
/>
</FormField>
<FormField label="1.6 Организация" htmlFor="Companies" required full>
<CompanyPickerMulti <CompanyPickerMulti
context={context} context={context}
id="Companies" id="Companies"
@@ -59,7 +43,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
placeholder="Начните вводить название организации…" placeholder="Начните вводить название организации…"
/> />
</FormField> </FormField>
<FormField label="1.7 Объект/Проект" htmlFor="ObjectProject" required full> <FormField label="1.6 Объект/Проект" htmlFor="ObjectProject" required full>
<ProjectPicker <ProjectPicker
context={context} context={context}
id="ObjectProject" id="ObjectProject"
@@ -2,8 +2,13 @@ import * as React from 'react';
import { CollapseSection, FormField, TextInput } from '../../../shared/ui'; import { CollapseSection, FormField, TextInput } from '../../../shared/ui';
import { DepartmentPicker } from '../../../shared/dictionary'; import { DepartmentPicker } from '../../../shared/dictionary';
import { ISectionProps } from '../model'; import { ISectionProps } from '../model';
import { parseVacanciesCount } from '../vacancyReasons';
import { VacancyReasonsField } from './VacancyReasonsField';
export const DepartmentSection: React.FC<ISectionProps> = ({ data, set, context }) => ( export const DepartmentSection: React.FC<ISectionProps> = ({ data, set, context }) => {
const vacanciesCount = parseVacanciesCount(data.OpenVacanciesCount);
return (
<CollapseSection title="2. Отдел / Подразделение" icon="building"> <CollapseSection title="2. Отдел / Подразделение" icon="building">
<FormField label="2.1 Подразделение" htmlFor="Department" required full> <FormField label="2.1 Подразделение" htmlFor="Department" required full>
<DepartmentPicker <DepartmentPicker
@@ -24,5 +29,19 @@ export const DepartmentSection: React.FC<ISectionProps> = ({ data, set, context
onChange={(v) => set('OpenVacanciesCount', v !== '' && Number(v) < 0 ? '0' : v)} onChange={(v) => set('OpenVacanciesCount', v !== '' && Number(v) < 0 ? '0' : v)}
/> />
</FormField> </FormField>
{/* Сразу под количеством: от него зависит, одна причина или несколько. */}
<FormField
label={vacanciesCount > 1 ? '2.3 Причины открытия вакансий' : '2.3 Причина открытия вакансии'}
fieldKey="VacancyReasons"
required
full
>
<VacancyReasonsField
value={data.VacancyReasons}
onChange={(v) => set('VacancyReasons', v)}
vacanciesCount={vacanciesCount}
/>
</FormField>
</CollapseSection> </CollapseSection>
); );
};
@@ -13,7 +13,7 @@ interface IVacancyReasonsFieldProps {
} }
/** /**
* 1.5 Причины открытия вакансии. * 2.3 Причины открытия вакансии.
* *
* Одна вакансия — выпадающий список, одна причина, один сотрудник. Больше одной * Одна вакансия — выпадающий список, одна причина, один сотрудник. Больше одной
* — флажки: несколько причин, у причин с ФИО — несколько сотрудников. Если * — флажки: несколько причин, у причин с ФИО — несколько сотрудников. Если
@@ -7,7 +7,7 @@ import {
} from './model'; } from './model';
/** /**
* Причины открытия вакансии (п. 1.5): несколько причин при 2.2 > 1 и несколько * Причины открытия вакансии (п. 2.3): несколько причин при 2.2 > 1 и несколько
* сотрудников на причину. Логика без UI — разбор/сводка/проверки. * сотрудников на причину. Логика без UI — разбор/сводка/проверки.
* *
* Хранение: полная структура — JSON в VacancyReasonsJson (из неё читает * Хранение: полная структура — JSON в VacancyReasonsJson (из неё читает
@@ -113,7 +113,7 @@ export function allowsMultipleReasons(vacanciesCount: number): boolean {
/** Блокирующая проблема для отправки: несколько причин при одной вакансии. */ /** Блокирующая проблема для отправки: несколько причин при одной вакансии. */
export function vacancyReasonsError(reasons: IVacancyReason[], vacanciesCount: number): string | undefined { export function vacancyReasonsError(reasons: IVacancyReason[], vacanciesCount: number): string | undefined {
if (!allowsMultipleReasons(vacanciesCount) && reasons.length > 1) { if (!allowsMultipleReasons(vacanciesCount) && reasons.length > 1) {
return 'При одной вакансии оставьте одну причину открытия (п. 1.5) или увеличьте количество вакансий (п. 2.2).'; return 'При одной вакансии оставьте одну причину открытия (п. 2.3) или увеличьте количество вакансий (п. 2.2).';
} }
return undefined; return undefined;
} }
@@ -122,7 +122,7 @@ export function vacancyReasonsError(reasons: IVacancyReason[], vacanciesCount: n
export function vacancyReasonsWarning(reasons: IVacancyReason[], vacanciesCount: number): string | undefined { export function vacancyReasonsWarning(reasons: IVacancyReason[], vacanciesCount: number): string | undefined {
const employees = cleanVacancyReasons(reasons).reduce((sum, r) => sum + r.employees.length, 0); const employees = cleanVacancyReasons(reasons).reduce((sum, r) => sum + r.employees.length, 0);
if (vacanciesCount > 0 && employees > vacanciesCount) { if (vacanciesCount > 0 && employees > vacanciesCount) {
return `Указано сотрудников: ${employees}, а вакансий: ${vacanciesCount}. Проверьте п. 1.5 и 2.2.`; return `Указано сотрудников: ${employees}, а вакансий: ${vacanciesCount}. Проверьте п. 2.2 и 2.3.`;
} }
return undefined; return undefined;
} }
@@ -17,10 +17,10 @@ import { parseVacanciesCount, vacancyReasonsError } from './vacancyReasons';
const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [ const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [
{ label: '1.3 дата подачи заявки', ok: (d) => !!d.SubmitDate }, { label: '1.3 дата подачи заявки', ok: (d) => !!d.SubmitDate },
{ label: '1.4 крайний срок закрытия', ok: (d) => !!d.ClosingDeadline }, { label: '1.4 крайний срок закрытия', ok: (d) => !!d.ClosingDeadline },
{ label: '1.5 причина открытия вакансии', ok: (d) => d.VacancyReasons.length > 0 }, { label: '1.5 организация', ok: (d) => d.Companies.length > 0 },
{ label: '1.6 организация', ok: (d) => d.Companies.length > 0 },
{ label: '2.1 подразделение', ok: (d) => !!d.Department.trim() }, { label: '2.1 подразделение', ok: (d) => !!d.Department.trim() },
{ label: '2.2 количество открытых вакансий', ok: (d) => !!d.OpenVacanciesCount.trim() }, { label: '2.2 количество открытых вакансий', ok: (d) => !!d.OpenVacanciesCount.trim() },
{ label: '2.3 причина открытия вакансии', ok: (d) => d.VacancyReasons.length > 0 },
{ label: '3.2 характер занятости', ok: (d) => !!d.EmploymentType }, { label: '3.2 характер занятости', ok: (d) => !!d.EmploymentType },
{ label: '3.3 функционал', ok: (d) => !!d.JobFunction.trim() }, { label: '3.3 функционал', ok: (d) => !!d.JobFunction.trim() },
{ label: '3.4 компетенции', ok: (d) => !!d.Competencies.trim() }, { label: '3.4 компетенции', ok: (d) => !!d.Competencies.trim() },