feat: причина «Декрет» в подборе и приёме, тип приёма подтягивается из подбора
Tests / frontend-test (push) Successful in 1m13s

This commit is contained in:
Тимур Городецкий
2026-09-24 16:38:06 +05:00
parent 17e7c634d5
commit 852b16c327
9 changed files with 111 additions and 12 deletions
@@ -0,0 +1,38 @@
import { hireTypeFromSelection } from './hireTypeFromSelection';
import { HIRE_TYPE_OPTIONS } from './model';
describe('hireTypeFromSelection', () => {
it('«Декрет» → тип «Декрет» + ФИО замещаемого', () => {
expect(hireTypeFromSelection('Декрет', 'Иванова Анна Ивановна')).toEqual({
hireType: 'Декрет',
replacedEmployee: 'Иванова Анна Ивановна'
});
});
it('«Замена уволенного сотрудника» → «Вместо уволенного» + ФИО', () => {
expect(hireTypeFromSelection('Замена уволенного сотрудника', 'Петров Пётр Петрович')).toEqual({
hireType: 'Вместо уволенного',
replacedEmployee: 'Петров Пётр Петрович'
});
});
it('«Новая штатная единица» → «Новый сотрудник» без ФИО', () => {
expect(hireTypeFromSelection('Новая штатная единица', 'что-то')).toEqual({
hireType: 'Новый сотрудник',
replacedEmployee: ''
});
});
it('перевод, «Другое» и пустая причина — без подстановки', () => {
expect(hireTypeFromSelection('Перевод сотрудника', 'Сидоров')).toBeUndefined();
expect(hireTypeFromSelection('Другое', 'уточнение')).toBeUndefined();
expect(hireTypeFromSelection('', '')).toBeUndefined();
});
it('подставляемый тип всегда есть среди вариантов «Тип приёма»', () => {
const allowed = HIRE_TYPE_OPTIONS.map((o) => o.value);
['Декрет', 'Замена уволенного сотрудника', 'Новая штатная единица'].forEach((reason) => {
expect(allowed).toContain(hireTypeFromSelection(reason, '')?.hireType);
});
});
});
@@ -0,0 +1,36 @@
import {
OPEN_REASON_REPLACE,
OPEN_REASON_NEW,
OPEN_REASON_MATERNITY
} from '../RecruitingSelection/model';
import { HIRE_TYPE_NEW, HIRE_TYPE_REPLACEMENT, HIRE_TYPE_MATERNITY } from './model';
/**
* Причина открытия вакансии (заявка на подбор) → тип приёма (заявка на приём).
* «Перевод сотрудника» и «Другое» однозначного соответствия не имеют — их тут
* нет, тип приёма в таком случае выбирает инициатор сам.
*/
const REASON_TO_HIRE_TYPE: Record<string, string> = {
[OPEN_REASON_REPLACE]: HIRE_TYPE_REPLACEMENT,
[OPEN_REASON_MATERNITY]: HIRE_TYPE_MATERNITY,
[OPEN_REASON_NEW]: HIRE_TYPE_NEW
};
/** Типы приёма, для которых переносим ФИО замещаемого сотрудника. */
const HIRE_TYPES_WITH_REPLACED_EMPLOYEE = [HIRE_TYPE_REPLACEMENT, HIRE_TYPE_MATERNITY];
export interface IHireTypePrefill {
hireType: string;
replacedEmployee: string;
}
/**
* Что подставить в «Тип приёма» / «Взамен кого» по связанной заявке на подбор.
* undefined — соответствия нет, поля приёма не трогаем.
*/
export function hireTypeFromSelection(openReason: string, openReasonDetails: string): IHireTypePrefill | undefined {
const hireType = REASON_TO_HIRE_TYPE[openReason];
if (!hireType) return undefined;
const replacedEmployee = HIRE_TYPES_WITH_REPLACED_EMPLOYEE.indexOf(hireType) !== -1 ? openReasonDetails : '';
return { hireType, replacedEmployee };
}
@@ -124,13 +124,19 @@ export const EMPTY_DATA: IFormData = {
/** Подпись флажка HireOnArrival рядом с датой приёма. */ /** Подпись флажка HireOnArrival рядом с датой приёма. */
export const HIRE_ON_ARRIVAL_LABEL = 'По приезду на объект'; export const HIRE_ON_ARRIVAL_LABEL = 'По приезду на объект';
export const HIRE_TYPE_NEW = 'Новый сотрудник';
/** Значение «Тип приёма», при котором показывается поле «Взамен кого». */ /** Значение «Тип приёма», при котором показывается поле «Взамен кого». */
export const HIRE_TYPE_REPLACEMENT = 'Вместо уволенного'; export const HIRE_TYPE_REPLACEMENT = 'Вместо уволенного';
/** Приём на время декретного отпуска сотрудника — тоже спрашиваем «Взамен кого». */
export const HIRE_TYPE_MATERNITY = 'Декрет';
/** Варианты «Тип приёма» — совпадают с choices колонки HireType. */ /** Варианты «Тип приёма» — совпадают с choices колонки HireType. */
export const HIRE_TYPE_OPTIONS: ISelectOption[] = [ export const HIRE_TYPE_OPTIONS: ISelectOption[] = [
{ value: 'Новый сотрудник', label: 'Новый сотрудник' }, { value: HIRE_TYPE_NEW, label: HIRE_TYPE_NEW },
{ value: HIRE_TYPE_REPLACEMENT, label: HIRE_TYPE_REPLACEMENT } { value: HIRE_TYPE_REPLACEMENT, label: HIRE_TYPE_REPLACEMENT },
{ value: HIRE_TYPE_MATERNITY, label: HIRE_TYPE_MATERNITY }
]; ];
/** Значение-триггер «Другое»: при нём показываем поле ручного ввода. */ /** Значение-триггер «Другое»: при нём показываем поле ручного ввода. */
@@ -9,7 +9,7 @@
{ "internalName": "CompanySharesJson", "title": "Распределение ЗП по компаниям (JSON)", "type": "Note" }, { "internalName": "CompanySharesJson", "title": "Распределение ЗП по компаниям (JSON)", "type": "Note" },
{ "internalName": "Phone", "title": "Телефон", "type": "Text" }, { "internalName": "Phone", "title": "Телефон", "type": "Text" },
{ "internalName": "HireType", "title": "Тип приёма", "type": "Choice", "choices": ["Новый сотрудник", "Вместо уволенного"] }, { "internalName": "HireType", "title": "Тип приёма", "type": "Choice", "choices": ["Новый сотрудник", "Вместо уволенного", "Декрет"] },
{ "internalName": "HireReplacedEmployee", "title": "Взамен кого (Ф.И.О. уволенного)", "type": "Text" }, { "internalName": "HireReplacedEmployee", "title": "Взамен кого (Ф.И.О. уволенного)", "type": "Text" },
{ "internalName": "Project", "title": "Проект", "type": "Text" }, { "internalName": "Project", "title": "Проект", "type": "Text" },
@@ -1,17 +1,23 @@
import * as React from 'react'; import * as React from 'react';
import { CollapseSection, FormField, TextInput, PhoneInput, Select } from '../../../shared/ui'; import { CollapseSection, FormField, TextInput, PhoneInput, Select } from '../../../shared/ui';
import { CompanyPickerMulti, ProjectPicker, ICompany } from '../../../shared/dictionary'; import { CompanyPickerMulti, ProjectPicker, ICompany } from '../../../shared/dictionary';
import { ISectionProps, HIRE_TYPE_OPTIONS, HIRE_TYPE_REPLACEMENT } from '../model'; import { ISectionProps, HIRE_TYPE_OPTIONS, HIRE_TYPE_REPLACEMENT, HIRE_TYPE_MATERNITY } from '../model';
import { SelectionRequestField } from './SelectionRequestField'; import { SelectionRequestField } from './SelectionRequestField';
import { CompanySharesField } from './CompanySharesField'; import { CompanySharesField } from './CompanySharesField';
/** Подпись поля «Взамен кого» — уточняем, чьё ФИО вписывать, по типу приёма. */
const REPLACED_EMPLOYEE_LABELS: Record<string, string> = {
[HIRE_TYPE_REPLACEMENT]: 'Взамен кого (Ф.И.О. уволенного, полностью, пример: Иванов Иван Иванович)',
[HIRE_TYPE_MATERNITY]: 'Взамен кого (Ф.И.О. сотрудника в декрете, полностью, пример: Иванова Анна Ивановна)'
};
export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context }) => { export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context }) => {
const isReplacement = data.HireType === HIRE_TYPE_REPLACEMENT; const replacedEmployeeLabel = REPLACED_EMPLOYEE_LABELS[data.HireType];
const handleHireTypeChange = (value: string): void => { const handleHireTypeChange = (value: string): void => {
set('HireType', value); set('HireType', value);
if (value !== HIRE_TYPE_REPLACEMENT) { if (!REPLACED_EMPLOYEE_LABELS[value]) {
set('HireReplacedEmployee', ''); // при не-замене очищаем скрытое поле set('HireReplacedEmployee', ''); // тип без замещения — очищаем скрытое поле
} }
}; };
@@ -77,9 +83,9 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
placeholder="— выберите —" placeholder="— выберите —"
/> />
</FormField> </FormField>
{isReplacement && ( {replacedEmployeeLabel && (
<FormField <FormField
label="Взамен кого (Ф.И.О. уволенного, полностью, пример: Иванов Иван Иванович)" label={replacedEmployeeLabel}
htmlFor="HireReplacedEmployee" htmlFor="HireReplacedEmployee"
full full
> >
@@ -5,6 +5,7 @@ import { routeUrl } from '../../../shared/routing';
import { getApprovedSelectionRequests, ISelectionRequestOption } from '../selectionLookup'; import { getApprovedSelectionRequests, ISelectionRequestOption } from '../selectionLookup';
import { loadSelectionRequest } from '../../RecruitingSelection/load'; import { loadSelectionRequest } from '../../RecruitingSelection/load';
import { IFormData, SetField } from '../model'; import { IFormData, SetField } from '../model';
import { hireTypeFromSelection } from '../hireTypeFromSelection';
/** Слаг формы подбора в реестре форм — цель ссылки (rptype). */ /** Слаг формы подбора в реестре форм — цель ссылки (rptype). */
const SELECTION_FORM_KEY = 'recruiting-selection'; const SELECTION_FORM_KEY = 'recruiting-selection';
@@ -104,6 +105,11 @@ export const SelectionRequestField: React.FC<ISelectionRequestFieldProps> = ({ d
set('ProbationPeriodOther', selection.SelectionProbationPeriodOther); set('ProbationPeriodOther', selection.SelectionProbationPeriodOther);
set('SalaryProbation', selection.RateProbation); set('SalaryProbation', selection.RateProbation);
set('SalaryAfterProbation', selection.RateAfterProbation); set('SalaryAfterProbation', selection.RateAfterProbation);
const hireTypePrefill = hireTypeFromSelection(selection.VacancyOpenReason, selection.VacancyOpenReasonDetails);
if (hireTypePrefill) {
set('HireType', hireTypePrefill.hireType);
set('HireReplacedEmployee', hireTypePrefill.replacedEmployee);
}
}) })
.catch(() => undefined); // связь уже сохранена по id/коду — сбой обогащения не критичен .catch(() => undefined); // связь уже сохранена по id/коду — сбой обогащения не критичен
}; };
@@ -161,12 +161,15 @@ export const OTHER_VALUE = 'Другое';
export const OPEN_REASON_REPLACE = 'Замена уволенного сотрудника'; export const OPEN_REASON_REPLACE = 'Замена уволенного сотрудника';
export const OPEN_REASON_TRANSFER = 'Перевод сотрудника'; export const OPEN_REASON_TRANSFER = 'Перевод сотрудника';
export const OPEN_REASON_NEW = 'Новая штатная единица'; export const OPEN_REASON_NEW = 'Новая штатная единица';
/** Декрет — временная вакансия на время отпуска сотрудника; ФИО замещаемого нужно кадрам для срочного договора. */
export const OPEN_REASON_MATERNITY = 'Декрет';
/** Варианты «Причина открытия вакансии» — совпадают с choices колонки VacancyOpenReason. */ /** Варианты «Причина открытия вакансии» — совпадают с choices колонки VacancyOpenReason. */
export const OPEN_REASON_OPTIONS: ISelectOption[] = [ export const OPEN_REASON_OPTIONS: ISelectOption[] = [
{ value: OPEN_REASON_REPLACE, label: OPEN_REASON_REPLACE }, { value: OPEN_REASON_REPLACE, label: OPEN_REASON_REPLACE },
{ value: OPEN_REASON_TRANSFER, label: OPEN_REASON_TRANSFER }, { value: OPEN_REASON_TRANSFER, label: OPEN_REASON_TRANSFER },
{ value: OPEN_REASON_NEW, label: OPEN_REASON_NEW }, { value: OPEN_REASON_NEW, label: OPEN_REASON_NEW },
{ value: OPEN_REASON_MATERNITY, label: OPEN_REASON_MATERNITY },
{ value: OTHER_VALUE, label: OTHER_VALUE } { value: OTHER_VALUE, label: OTHER_VALUE }
]; ];
@@ -5,7 +5,7 @@
{ "internalName": "RequestStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] }, { "internalName": "RequestStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
{ "internalName": "Responsible", "title": "Ответственный за подачу заявки", "type": "User", "userSelectionMode": "PeopleOnly", "required": true }, { "internalName": "Responsible", "title": "Ответственный за подачу заявки", "type": "User", "userSelectionMode": "PeopleOnly", "required": true },
{ "internalName": "SubmitDate", "title": "Дата подачи заявки", "type": "DateTime", "dateOnly": true }, { "internalName": "SubmitDate", "title": "Дата подачи заявки", "type": "DateTime", "dateOnly": true },
{ "internalName": "VacancyOpenReason", "title": "Причина открытия вакансии", "type": "Choice", "choices": ["Замена уволенного сотрудника", "Перевод сотрудника", "Новая штатная единица", "Другое"] }, { "internalName": "VacancyOpenReason", "title": "Причина открытия вакансии", "type": "Choice", "choices": ["Замена уволенного сотрудника", "Перевод сотрудника", "Новая штатная единица", "Декрет", "Другое"] },
{ "internalName": "VacancyOpenReasonDetails", "title": "Причина открытия — ФИО / уточнение", "type": "Text" }, { "internalName": "VacancyOpenReasonDetails", "title": "Причина открытия — ФИО / уточнение", "type": "Text" },
{ "internalName": "CompaniesJson", "title": "Компании (JSON)", "type": "Note" }, { "internalName": "CompaniesJson", "title": "Компании (JSON)", "type": "Note" },
{ "internalName": "ObjectProject", "title": "Объект/Проект", "type": "Text" }, { "internalName": "ObjectProject", "title": "Объект/Проект", "type": "Text" },
@@ -7,14 +7,18 @@ import {
OPEN_REASON_OPTIONS, OPEN_REASON_OPTIONS,
OPEN_REASON_REPLACE, OPEN_REASON_REPLACE,
OPEN_REASON_TRANSFER, OPEN_REASON_TRANSFER,
OPEN_REASON_NEW OPEN_REASON_NEW,
OPEN_REASON_MATERNITY
} from '../model'; } from '../model';
/** Причины, где вакансия открывается под конкретного сотрудника — спрашиваем его ФИО. */
const REASONS_WITH_EMPLOYEE_NAME = [OPEN_REASON_REPLACE, OPEN_REASON_TRANSFER, OPEN_REASON_MATERNITY];
export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context, onResponsibleChange }) => { export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context, onResponsibleChange }) => {
const reason = data.VacancyOpenReason; const reason = data.VacancyOpenReason;
const showReasonDetails = reason !== '' && reason !== OPEN_REASON_NEW; const showReasonDetails = reason !== '' && reason !== OPEN_REASON_NEW;
const reasonDetailsLabel = const reasonDetailsLabel =
reason === OPEN_REASON_REPLACE || reason === OPEN_REASON_TRANSFER REASONS_WITH_EMPLOYEE_NAME.indexOf(reason) !== -1
? 'ФИО сотрудника (Полностью, пример: Иванов Иван Иванович)' ? 'ФИО сотрудника (Полностью, пример: Иванов Иван Иванович)'
: 'Уточните причину'; : 'Уточните причину';