feat: поля вида «Инициатива работодателя» и короткие названия видов

Девятый вид: причина (из ТЗ + «Другое»), что меняется — график работы
и/или оклад; оклад вводится один, долю постоянной части можно задать
(по умолчанию 75 % из ТЗ), переменная — остаток до 100 %, суммы считаются
сами; даты не раньше чем через 15 дней (срок уведомления работника);
обоснование — текстом или документом. Длинные названия видов 7 и 9
сокращены, а дублирующий выбранный вид заголовок над полями убран у всех
видов.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-29 15:42:56 +05:00
parent f61418dbe2
commit bdd8da9b89
24 changed files with 567 additions and 63 deletions
@@ -65,7 +65,8 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
| Продление срока ТД | — | **пропущен** (2026-10-29): по ТЗ это автоматический запрос ОК за месяц до окончания срочного ТД (маршрут В), а не обычная заявка — решать отдельно |
| Изменение режима / графика работы | `ScheduleChange` | по ролям строго по ТЗ. Общее: текущий график + текущее время работы (списки как в подборе/приёме, «Другое» → текст; `OptionWithOtherField`, наборы — `byType/workSchedule.ts`), дата начала, вложение «Новый график» (необязательно). **Работник:** неполный рабочий день (часов) / неполная рабочая неделя (дней) — одно или оба, текст заявления. **Работодатель:** новый режим (вахтовый / сменный / иной + описание), новые график и время работы, причина (специфика объекта / смещение сроков проекта — только из ТЗ), обоснование. Поля другой роли при сохранении очищаются |
| Изменение трудовой функции (без смены должности) | `FunctionChange` | ровно по шаблону ТЗ: дата изменения, «В связи с» (свободный текст — списка причин в ТЗ нет), вложение **«Новая должностная инструкция»** — обязательно. Инициатор — только руководитель |
| Остальные 2 | — | ещё не разобраны с кадрами |
| Изменение условий труда по инициативе работодателя | `EmployerInitiative` | причина (из названия вида в ТЗ: экономические / технологические / изменения в организации / сокращение объёма работ + «Другое» со своим текстом); «Что меняется» — флажки а) график работы, б) должностной оклад (можно оба, при отправке — хотя бы один); а) текущий → новый график + дата; б) **один новый оклад + доля постоянной части в %** (по умолчанию 75 из ТЗ, 0–100, можно дробную), переменная доля = остаток до 100 %, суммы обеих частей в ₸ считаются сами (`salaryParts.ts`, превысить 100 % нельзя; в SP пишутся при сохранении) + дата; даты **не раньше +15 дней** (уведомление по ТЗ: блок в календаре + проверка при отправке); обоснование — **текстом ИЛИ документом**, одно из двух обязательно при отправке |
| Остальные 1 | — | ещё не разобран с кадрами |
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
@@ -1,8 +1,7 @@
import * as React from 'react';
import { CollapseSection, FormField, MoneyInput, TextInput, TextArea, Select, Checkbox } from '../../../../shared/ui';
import { FormGrid, FormField, MoneyInput, TextInput, TextArea, Select, Checkbox } from '../../../../shared/ui';
import { useFormReadOnly } from '@tnsu/ui-kit';
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { ALLOWANCE_REASONS, ATTESTATION_ATTACHMENT, requiresAttestation } from './options';
import { UnitSwitch } from './UnitSwitch';
@@ -15,13 +14,13 @@ import styles from './AllowanceSection.module.scss';
* (в т.ч. объём доп. работы при совмещении). Поля необязательные; вложение
* «Результаты аттестации» — только при основании «г» и там обязательно (по ТЗ).
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const AllowanceSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
const readOnly = useFormReadOnly();
return (
<CollapseSection title={CHANGE_TYPE_LABELS.allowance} icon="calculator">
<FormGrid>
<FormField label="Основание" htmlFor="AllowanceReason" full>
<Select
id="AllowanceReason"
@@ -99,6 +98,6 @@ export const AllowanceSection: React.FC<ISectionProps> = ({ data, set, attachmen
required
/>
)}
</CollapseSection>
</FormGrid>
);
};
@@ -0,0 +1,6 @@
// Поля вида идут сразу под шапкой (сотрудник · вид · инициатор) отдельной сеткой
// FormGrid. У сетки ui-kit нет внешнего отступа, а расстояние между строками полей —
// 18px (row-gap FormGrid): тот же отступ, чтобы стык двух сеток был не виден.
.typeFields {
margin-top: 18px;
}
@@ -8,6 +8,8 @@ import { DepartmentTransferSection } from './DepartmentTransfer/DepartmentTransf
import { WorkplaceChangeSection } from './WorkplaceChange/WorkplaceChangeSection';
import { ScheduleChangeSection } from './ScheduleChange/ScheduleChangeSection';
import { FunctionChangeSection } from './FunctionChange/FunctionChangeSection';
import { EmployerInitiativeSection } from './EmployerInitiative/EmployerInitiativeSection';
import styles from './ChangeTypeSections.module.scss';
/**
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
@@ -21,15 +23,23 @@ const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
'department-transfer': DepartmentTransferSection,
'workplace-change': WorkplaceChangeSection,
'schedule-change': ScheduleChangeSection,
'function-change': FunctionChangeSection
'function-change': FunctionChangeSection,
'employer-initiative': EmployerInitiativeSection
};
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
/**
* Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES).
* Заголовка у секции нет — вид выбран прямо над ней; обёртка даёт только отступ от шапки.
*/
export const ChangeTypeSections: React.FC<ISectionProps> = (props) => (
<>
{props.data.ChangeTypes.map((type) => {
const Section = SECTION_BY_TYPE[type];
return Section ? <Section key={type} {...props} /> : undefined;
return Section ? (
<div key={type} className={styles.typeFields}>
<Section {...props} />
</div>
) : undefined;
})}
</>
);
@@ -1,8 +1,7 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, PeopleField } from '../../../../shared/ui';
import { FormGrid, FormField, TextInput, PeopleField } from '../../../../shared/ui';
import { InitiatorReasonFields } from '../InitiatorReasonFields';
import { DepartmentPicker } from '../../../../shared/dictionary';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { DEPARTMENT_TRANSFER_REASONS } from './options';
@@ -12,10 +11,10 @@ import { DEPARTMENT_TRANSFER_REASONS } from './options';
* принимающий руководитель. Его согласие по ТЗ — шаг маршрута: форма сама ставит
* его первым шагом согласования (см. receivingHead.ts). Обязателен при отправке.
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const DepartmentTransferSection: React.FC<ISectionProps> = ({ data, set, context }) => (
<CollapseSection title={CHANGE_TYPE_LABELS['department-transfer']} icon="people">
<FormGrid>
<FormField label="Новое подразделение" htmlFor="DepartmentTransferNewDepartment">
<DepartmentPicker
context={context}
@@ -59,5 +58,5 @@ export const DepartmentTransferSection: React.FC<ISectionProps> = ({ data, set,
onChange: (v) => set('DepartmentTransferJustification', v)
}}
/>
</CollapseSection>
</FormGrid>
);
@@ -0,0 +1,15 @@
@import '../../../../../../styles/design-tokens';
// Флажки «что меняется» — в строку, на узком экране переносятся.
.flags {
display: flex;
flex-wrap: wrap;
gap: $sp-2 $sp-6;
}
// Пояснение под датой (срок уведомления работника по ТЗ).
.hint {
margin-top: $sp-1;
font-size: $fs-label;
color: $gray-500;
}
@@ -0,0 +1,179 @@
import * as React from 'react';
import { FormGrid, FormField, TextInput, TextArea, Select, Checkbox, MoneyInput } from '../../../../shared/ui';
import { useFormReadOnly } from '@tnsu/ui-kit';
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
import { ISectionProps } from '../../model';
import { OptionWithOtherField } from '../OptionWithOtherField';
import { WORK_SCHEDULE_OPTIONS } from '../workSchedule';
import {
EMPLOYER_CHANGE_REASONS,
EMPLOYER_REASON_OTHER,
EMPLOYER_CHANGE_NOTICE_HINT,
EMPLOYER_JUSTIFICATION_ATTACHMENT,
minEmployerChangeDate
} from './options';
import { splitSalary, sanitizeFixedPercent } from './salaryParts';
import styles from './EmployerInitiativeSection.module.scss';
/**
* Поля вида «Изменение условий труда по инициативе работодателя» — по шаблону ТЗ:
* «в связи с ___ (причина) прошу: а) изменить график работы с ___ на ___ с …;
* б) изменить должностной оклад: постоянная часть 75 % — ___, переменная 25 % — ___, с …».
* Что меняется — флажки а/б (можно оба); оклад вводится один, доля постоянной части
* задаётся (по умолчанию 75 %), переменная — остаток до 100 %, суммы считаются сами. Даты — не раньше чем через 15 дней (уведомление работника по ТЗ).
* Обоснование — текстом или документом (одно из двух обязательно при отправке).
*
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const EmployerInitiativeSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
const readOnly = useFormReadOnly();
const minDate = minEmployerChangeDate();
const parts = splitSalary(data.EmployerChangeNewSalary, data.EmployerChangeFixedPercent);
return (
<FormGrid>
<FormField label="Причина" htmlFor="EmployerChangeReason" full>
<Select
id="EmployerChangeReason"
value={data.EmployerChangeReason}
onChange={(v) => {
set('EmployerChangeReason', v);
if (v !== EMPLOYER_REASON_OTHER) set('EmployerChangeReasonOther', '');
}}
options={EMPLOYER_CHANGE_REASONS}
placeholder="— выберите —"
/>
</FormField>
{data.EmployerChangeReason === EMPLOYER_REASON_OTHER && (
<FormField label="Причина (другое)" htmlFor="EmployerChangeReasonOther" full>
<TextInput
id="EmployerChangeReasonOther"
value={data.EmployerChangeReasonOther}
onChange={(v) => set('EmployerChangeReasonOther', v)}
/>
</FormField>
)}
<FormField label="Что меняется" htmlFor="EmployerChangeSchedule" full>
<div className={styles.flags}>
<Checkbox
checked={data.EmployerChangeSchedule}
onChange={(checked) => set('EmployerChangeSchedule', checked)}
label="График работы"
/>
<Checkbox
checked={data.EmployerChangeSalary}
onChange={(checked) => set('EmployerChangeSalary', checked)}
label="Должностной оклад"
/>
</div>
</FormField>
{data.EmployerChangeSchedule && (
<>
<OptionWithOtherField
label="Текущий график работы"
id="EmployerChangeCurrentSchedule"
value={data.EmployerChangeCurrentSchedule}
otherValue={data.EmployerChangeCurrentScheduleOther}
options={WORK_SCHEDULE_OPTIONS}
onChange={(v) => set('EmployerChangeCurrentSchedule', v)}
onOtherChange={(v) => set('EmployerChangeCurrentScheduleOther', v)}
/>
<OptionWithOtherField
label="Новый график работы"
id="EmployerChangeNewSchedule"
value={data.EmployerChangeNewSchedule}
otherValue={data.EmployerChangeNewScheduleOther}
options={WORK_SCHEDULE_OPTIONS}
onChange={(v) => set('EmployerChangeNewSchedule', v)}
onOtherChange={(v) => set('EmployerChangeNewScheduleOther', v)}
/>
<FormField label="Дата изменения графика" htmlFor="EmployerChangeScheduleDate" full>
<TextInput
id="EmployerChangeScheduleDate"
type="date"
value={data.EmployerChangeScheduleDate}
onChange={(v) => set('EmployerChangeScheduleDate', v)}
min={minDate}
/>
{!readOnly && <div className={styles.hint}>{EMPLOYER_CHANGE_NOTICE_HINT}</div>}
</FormField>
</>
)}
{data.EmployerChangeSalary && (
<>
<FormField label="Новый должностной оклад, ₸" htmlFor="EmployerChangeNewSalary" full>
<MoneyInput
id="EmployerChangeNewSalary"
value={data.EmployerChangeNewSalary}
onChange={(v) => set('EmployerChangeNewSalary', v)}
/>
</FormField>
{/* Задаётся только доля постоянной части; переменная — остаток до 100 %,
суммы считаются из оклада (см. salaryParts.ts) — превысить 100 % нельзя. */}
<FormField label="Постоянная (повременная) часть, %" htmlFor="EmployerChangeFixedPercent">
<TextInput
id="EmployerChangeFixedPercent"
value={data.EmployerChangeFixedPercent}
onChange={(v) => set('EmployerChangeFixedPercent', sanitizeFixedPercent(v))}
placeholder="например, 75"
/>
</FormField>
<FormField label="Переменная часть, %" htmlFor="EmployerChangeVariablePercent">
<TextInput
id="EmployerChangeVariablePercent"
value={parts ? String(parts.variablePercent) : ''}
onChange={() => undefined}
readOnly
/>
</FormField>
<FormField label="Постоянная часть, ₸" htmlFor="EmployerChangeFixedPart">
<MoneyInput
id="EmployerChangeFixedPart"
value={parts ? String(parts.fixed) : ''}
onChange={() => undefined}
readOnly
/>
</FormField>
<FormField label="Переменная часть, ₸" htmlFor="EmployerChangeVariablePart">
<MoneyInput
id="EmployerChangeVariablePart"
value={parts ? String(parts.variable) : ''}
onChange={() => undefined}
readOnly
/>
</FormField>
<FormField label="Дата изменения оклада" htmlFor="EmployerChangeSalaryDate" full>
<TextInput
id="EmployerChangeSalaryDate"
type="date"
value={data.EmployerChangeSalaryDate}
onChange={(v) => set('EmployerChangeSalaryDate', v)}
min={minDate}
/>
{!readOnly && <div className={styles.hint}>{EMPLOYER_CHANGE_NOTICE_HINT}</div>}
</FormField>
</>
)}
<FormField label="Обоснование" htmlFor="EmployerChangeJustification" full>
<TextArea
id="EmployerChangeJustification"
value={data.EmployerChangeJustification}
onChange={(v) => set('EmployerChangeJustification', v)}
/>
</FormField>
{attachments && (
<AttachmentSlotField
label={`${EMPLOYER_JUSTIFICATION_ATTACHMENT} (приказ о реорганизации, сокращении объёма и т. п.)`}
editor={attachments}
editable={!readOnly}
groupKey={EMPLOYER_JUSTIFICATION_ATTACHMENT}
category={EMPLOYER_JUSTIFICATION_ATTACHMENT}
/>
)}
</FormGrid>
);
};
@@ -0,0 +1,36 @@
import { ISelectOption } from '../../../../shared/ui';
import { isoDatePlusDays } from '../../../../shared/dateUtils';
/**
* Вид «Изменение условий труда по инициативе работодателя». Причины — из названия
* вида в ТЗ (экономические, технологические, изменения в организации, сокращение
* объёма работ) + «Другое» со своим текстом (решение Тимура 2026-10-29).
* Значения = choices колонки EmployerChangeReason.
*/
export const EMPLOYER_REASON_OTHER = 'Другое';
export const EMPLOYER_CHANGE_REASONS: ISelectOption[] = [
'Экономические причины',
'Технологические причины',
'Изменения в организации',
'Сокращение объёма работ',
EMPLOYER_REASON_OTHER
].map((reason) => ({ value: reason, label: reason }));
/**
* По ТЗ работника уведомляют не позднее чем за столько календарных дней до даты
* изменений — поэтому раньше дату не даём выбрать (как срок подачи в отпуске).
*/
export const EMPLOYER_CHANGE_NOTICE_DAYS = 15;
export const EMPLOYER_CHANGE_NOTICE_HINT = `Не раньше чем через ${EMPLOYER_CHANGE_NOTICE_DAYS} календарных дней: работника уведомляют за ${EMPLOYER_CHANGE_NOTICE_DAYS} дней до изменений.`;
/** Самая ранняя допустимая дата изменений, YYYY-MM-DD. */
export function minEmployerChangeDate(): string {
return isoDatePlusDays(EMPLOYER_CHANGE_NOTICE_DAYS);
}
/**
* Вложение-обоснование (приказ о реорганизации, сокращении объёма и т.п.). По ТЗ
* «ТЕКСТ ИЛИ ПРИЛОЖИТЬ ДОКУМЕНТ» — сам по себе необязателен, но при отправке
* нужен текст обоснования или этот файл (см. submitChecks).
*/
export const EMPLOYER_JUSTIFICATION_ATTACHMENT = 'Документ-обоснование';
@@ -0,0 +1,43 @@
import { splitSalary, sanitizeFixedPercent } from './salaryParts';
describe('splitSalary', () => {
it('по умолчанию из ТЗ — 75 / 25', () => {
expect(splitSalary('400000', '75')).toEqual({ fixedPercent: 75, variablePercent: 25, fixed: 300000, variable: 100000 });
});
it('своя доля: переменная часть — остаток до 100 %', () => {
expect(splitSalary('400000', '70')).toEqual({ fixedPercent: 70, variablePercent: 30, fixed: 280000, variable: 120000 });
});
it('при округлении сумма частей всё равно равна окладу', () => {
const parts = splitSalary('333333', '75');
expect(parts?.fixed).toBe(250000);
expect((parts?.fixed ?? 0) + (parts?.variable ?? 0)).toBe(333333);
});
it('дробная доля', () => {
expect(splitSalary('200000', '62.5')).toEqual({
fixedPercent: 62.5,
variablePercent: 37.5,
fixed: 125000,
variable: 75000
});
});
it('нет оклада или доли — частей нет', () => {
expect(splitSalary('', '75')).toBeUndefined();
expect(splitSalary('0', '75')).toBeUndefined();
expect(splitSalary('400000', '')).toBeUndefined();
});
});
describe('sanitizeFixedPercent', () => {
it('больше 100 не даёт', () => {
expect(sanitizeFixedPercent('150')).toBe('100');
});
it('пропускает обычные и дробные доли', () => {
expect(sanitizeFixedPercent('75')).toBe('75');
expect(sanitizeFixedPercent('62,5')).toBe('62.5');
});
});
@@ -0,0 +1,50 @@
import { sanitizePercentInput } from '../Allowance/amount';
/**
* Новый оклад делится на постоянную (повременную) и переменную части. В шаблоне ТЗ
* — 75 % / 25 %; по решению Тимура (2026-10-29) долю постоянной части можно задать
* самому, а переменная — всегда остаток до 100 %. Так сумма частей ровно 100 % и
* превысить её нельзя в принципе.
*/
export const DEFAULT_FIXED_PERCENT = '75';
const MAX_PERCENT = 100;
export interface ISalaryParts {
/** Доли в процентах; variablePercent = 100 − fixedPercent. */
fixedPercent: number;
variablePercent: number;
/** Суммы в тенге; variable — остаток, чтобы fixed + variable = оклад. */
fixed: number;
variable: number;
}
/**
* Ввод доли постоянной части → «сырое» число строкой: только цифры и один
* разделитель (как ввод процента надбавки), не больше 100.
*/
export function sanitizeFixedPercent(text: string): string {
const percent = sanitizePercentInput(text);
return Number(percent) > MAX_PERCENT ? String(MAX_PERCENT) : percent;
}
/** Доля постоянной части числом; пусто/некорректно/вне 0–100 — undefined. */
function parsePercent(raw: string): number | undefined {
const percent = Number(raw);
if (raw.trim() === '' || !Number.isFinite(percent) || percent < 0 || percent > MAX_PERCENT) return undefined;
return percent;
}
/**
* Оклад и доля постоянной части → проценты и суммы обеих частей. Постоянная сумма
* округляется до тенге, переменная — остаток (без расхождений от округления).
* Нет оклада или доли — undefined.
*/
export function splitSalary(salaryRaw: string, fixedPercentRaw: string): ISalaryParts | undefined {
const salary = Number(salaryRaw);
const fixedPercent = parsePercent(fixedPercentRaw);
if (salaryRaw.trim() === '' || !Number.isFinite(salary) || salary <= 0 || fixedPercent === undefined) {
return undefined;
}
const fixed = Math.round((salary * fixedPercent) / MAX_PERCENT);
return { fixedPercent, variablePercent: MAX_PERCENT - fixedPercent, fixed, variable: salary - fixed };
}
@@ -1,8 +1,7 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea } from '../../../../shared/ui';
import { FormGrid, FormField, TextInput, TextArea } from '../../../../shared/ui';
import { useFormReadOnly } from '@tnsu/ui-kit';
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './options';
@@ -13,13 +12,13 @@ import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './options';
* текст, списка причин в ТЗ нет) и обязательная новая ДИ. Инициатор — только
* руководитель.
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const FunctionChangeSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
const readOnly = useFormReadOnly();
return (
<CollapseSection title={CHANGE_TYPE_LABELS['function-change']} icon="document">
<FormGrid>
<FormField label="Дата изменения" htmlFor="FunctionChangeDate">
<TextInput
id="FunctionChangeDate"
@@ -45,6 +44,6 @@ export const FunctionChangeSection: React.FC<ISectionProps> = ({ data, set, atta
required
/>
)}
</CollapseSection>
</FormGrid>
);
};
@@ -15,7 +15,7 @@ interface Props {
* Окончание секции вида, где по ТЗ шаблон зависит от инициатора:
* - работник — «заявление (текст)»: только «Текст заявления», без списка причин;
* - работодатель (и пока инициатор не выбран) — «в связи с: а) б) в)»: причина + обоснование.
* Поля кладутся прямо в сетку CollapseSection (без обёртки) — как и остальные поля вида.
* Поля кладутся прямо в сетку FormGrid секции (без обёртки) — как и остальные поля вида.
*/
export const InitiatorReasonFields: React.FC<Props> = ({ initiator, reason, justification }) => {
const isEmployee = initiator === INITIATOR_EMPLOYEE;
@@ -16,7 +16,7 @@ interface Props {
/**
* Выбор из списка + ручной ввод при «Другое» (как график/время работы в подборе
* и приёме). Выбрали не «Другое» — ручной текст очищается, чтобы не сохранить
* скрытое значение. Поля кладутся прямо в сетку CollapseSection.
* скрытое значение. Поля кладутся прямо в сетку FormGrid секции.
*/
export const OptionWithOtherField: React.FC<Props> = ({
label,
@@ -1,10 +1,9 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput } from '../../../../shared/ui';
import { FormGrid, FormField, TextInput } from '../../../../shared/ui';
import { InitiatorReasonFields } from '../InitiatorReasonFields';
import { useFormReadOnly } from '@tnsu/ui-kit';
import { PositionPicker } from '../../../../shared/dictionary';
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { POSITION_TRANSFER_REASONS, JOB_DESCRIPTION_ATTACHMENT } from './options';
@@ -14,13 +13,13 @@ import { POSITION_TRANSFER_REASONS, JOB_DESCRIPTION_ATTACHMENT } from './options
* причина (а–г), обоснование и обязательная должностная инструкция. Текущая
* должность уже видна в «Организации» — отдельного «было» нет.
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const PositionTransferSection: React.FC<ISectionProps> = ({ data, set, context, attachments }) => {
const readOnly = useFormReadOnly();
return (
<CollapseSection title={CHANGE_TYPE_LABELS['position-transfer']} icon="people">
<FormGrid>
<FormField label="Новая должность" htmlFor="PositionTransferNewPosition">
<PositionPicker
context={context}
@@ -62,6 +61,6 @@ export const PositionTransferSection: React.FC<ISectionProps> = ({ data, set, co
required
/>
)}
</CollapseSection>
</FormGrid>
);
};
@@ -1,6 +1,5 @@
import * as React from 'react';
import { CollapseSection, FormField, MoneyInput, TextInput, TextArea, Select } from '../../../../shared/ui';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { FormGrid, FormField, MoneyInput, TextInput, TextArea, Select } from '../../../../shared/ui';
import { ISectionProps } from '../../model';
import { SALARY_INCREASE_REASONS } from './reasons';
@@ -9,11 +8,10 @@ import { SALARY_INCREASE_REASONS } from './reasons';
* кадров: было/стало, дата, причина (а/б/в) и обоснование. Все необязательные,
* без ограничений — по решению от 2026-10-28.
*
* Поля — прямо в CollapseSection, без FormGrid: тело секции само является
* двухколоночной сеткой, вложенная FormGrid сжала бы всё в одну её ячейку.
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const SalaryIncreaseSection: React.FC<ISectionProps> = ({ data, set }) => (
<CollapseSection title={CHANGE_TYPE_LABELS['salary-increase']} icon="calculator">
<FormGrid>
<FormField label="Текущий оклад, ₸" htmlFor="SalaryIncreaseCurrent">
<MoneyInput
id="SalaryIncreaseCurrent"
@@ -52,5 +50,5 @@ export const SalaryIncreaseSection: React.FC<ISectionProps> = ({ data, set }) =>
onChange={(v) => set('SalaryIncreaseJustification', v)}
/>
</FormField>
</CollapseSection>
</FormGrid>
);
@@ -1,8 +1,8 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, Select } from '../../../../shared/ui';
import { FormGrid, FormField, TextInput, Select } from '../../../../shared/ui';
import { useFormReadOnly } from '@tnsu/ui-kit';
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
import { CHANGE_TYPE_LABELS, INITIATOR_EMPLOYEE } from '../../changeTypes';
import { INITIATOR_EMPLOYEE } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { InitiatorReasonFields } from '../InitiatorReasonFields';
import { OptionWithOtherField } from '../OptionWithOtherField';
@@ -21,14 +21,14 @@ import {
* (часов в день / дней в неделю) и текст заявления; работодатель — новый режим,
* новые график и время работы, причина и обоснование. Все поля необязательные.
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const ScheduleChangeSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
const readOnly = useFormReadOnly();
const isEmployee = data.ChangeInitiator === INITIATOR_EMPLOYEE;
return (
<CollapseSection title={CHANGE_TYPE_LABELS['schedule-change']} icon="calendar">
<FormGrid>
<OptionWithOtherField
label="Текущий график работы"
id="ScheduleChangeCurrentSchedule"
@@ -148,6 +148,6 @@ export const ScheduleChangeSection: React.FC<ISectionProps> = ({ data, set, atta
category={NEW_SCHEDULE_ATTACHMENT}
/>
)}
</CollapseSection>
</FormGrid>
);
};
@@ -1,7 +1,6 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea, Select, Toggle } from '../../../../shared/ui';
import { FormGrid, FormField, TextInput, TextArea, Select, Toggle } from '../../../../shared/ui';
import { ProjectPicker } from '../../../../shared/dictionary';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { WORKPLACE_CHANGE_REASONS } from './options';
import { WORK_SCHEDULE_OPTIONS } from '../workSchedule';
@@ -18,13 +17,13 @@ const PROJECT_PLACEHOLDER = 'Начните вводить название ил
* (текущая организация — из выбранной в шапке, новая обязательна), адрес, дата,
* причина (а–в), условия переезда, вахта с графиком работы, обоснование.
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
*/
export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, context }) => {
const companies = useCompanyOptions(context);
return (
<CollapseSection title={CHANGE_TYPE_LABELS['workplace-change']} icon="vehicle">
<FormGrid>
<FormField label="Текущая организация" htmlFor="WorkplaceChangeCurrentCompany">
{/* Та же, что выбрана в «Организации» сотрудника, — второй раз не вводим. */}
<TextInput id="WorkplaceChangeCurrentCompany" value={data.Employee?.company || ''} onChange={() => undefined} readOnly />
@@ -128,6 +127,6 @@ export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, con
onChange: (v) => set('WorkplaceChangeJustification', v)
}}
/>
</CollapseSection>
</FormGrid>
);
};
@@ -5,6 +5,7 @@ import { ATTESTATION_ATTACHMENT, requiresAttestation } from './Allowance/options
import { JOB_DESCRIPTION_ATTACHMENT } from './PositionTransfer/options';
import { NEW_SCHEDULE_ATTACHMENT } from './ScheduleChange/options';
import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './FunctionChange/options';
import { EMPLOYER_JUSTIFICATION_ATTACHMENT } from './EmployerInitiative/options';
/**
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
@@ -28,6 +29,10 @@ export function activeAttachmentSlots(data: IFormData): IAttachmentSlot[] {
if (data.ChangeTypes.includes('function-change')) {
slots.push({ category: NEW_JOB_DESCRIPTION_ATTACHMENT, required: true });
}
if (data.ChangeTypes.includes('employer-initiative')) {
// «Текст ИЛИ документ» — сам слот необязателен, правило «одно из двух» — в submitChecks.
slots.push({ category: EMPLOYER_JUSTIFICATION_ATTACHMENT, required: false });
}
if (data.ChangeTypes.includes('schedule-change')) {
// «Можно приложить документ» — необязательно.
slots.push({ category: NEW_SCHEDULE_ATTACHMENT, required: false });
@@ -44,14 +49,16 @@ export function prepareFilesFor(data: IFormData): PrepareFiles {
return (groupKey, files) => (visible.indexOf(groupKey) >= 0 ? prefixFiles(files, groupKey) : []);
}
/** Есть ли в слоте файл — уже прикреплённый или выбранный к загрузке. */
export function hasAttachment(editor: IAttachmentEditor, category: string): boolean {
const uploaded = attachmentsOfCategory(editor.items, category).length;
const selected = (editor.pending[category] || []).length;
return uploaded + selected > 0;
}
/** Подписи обязательных слотов, где нет ни прикреплённого, ни выбранного файла. */
export function missingRequiredAttachments(data: IFormData, editor: IAttachmentEditor): string[] {
return activeAttachmentSlots(data)
.filter((slot) => slot.required)
.filter((slot) => {
const uploaded = attachmentsOfCategory(editor.items, slot.category).length;
const selected = (editor.pending[slot.category] || []).length;
return uploaded + selected === 0;
})
.filter((slot) => slot.required && !hasAttachment(editor, slot.category))
.map((slot) => slot.category);
}
@@ -1,12 +1,20 @@
import { IAttachmentEditor } from '../../../shared/attachments/useAttachmentEditor';
import { formatIsoDate } from '../../../shared/dateUtils';
import {
IFormData,
INITIATOR_REQUIRED_MESSAGE,
NEW_COMPANY_REQUIRED_MESSAGE,
RECEIVING_HEAD_REQUIRED_MESSAGE
RECEIVING_HEAD_REQUIRED_MESSAGE,
EMPLOYER_CHANGE_NOTHING_SELECTED_MESSAGE,
EMPLOYER_JUSTIFICATION_REQUIRED_MESSAGE
} from '../model';
import { hasInitiatorChoice } from '../changeTypes';
import { missingRequiredAttachments } from './attachmentSlots';
import { hasAttachment, missingRequiredAttachments } from './attachmentSlots';
import {
EMPLOYER_CHANGE_NOTICE_DAYS,
EMPLOYER_JUSTIFICATION_ATTACHMENT,
minEmployerChangeDate
} from './EmployerInitiative/options';
/**
* Что обязательно по выбранному виду изменения — проверяется только при ОТПРАВКЕ:
@@ -25,5 +33,32 @@ export function changeTypeSubmitError(data: IFormData, attachments: IAttachmentE
if (data.ChangeTypes.includes('workplace-change') && !data.WorkplaceChangeNewCompanyId) {
return NEW_COMPANY_REQUIRED_MESSAGE;
}
if (data.ChangeTypes.includes('employer-initiative')) {
return employerInitiativeError(data, attachments);
}
return undefined;
}
/**
* Инициатива работодателя: отмечено, что меняется; обоснование текстом ИЛИ
* документом (ТЗ: «ТЕКСТ ИЛИ ПРИЛОЖИТЬ ДОКУМЕНТ»); даты — не раньше срока
* уведомления. Даты проверяем и здесь, а не только min в календаре: черновик мог
* пролежать, и прежняя дата стала слишком близкой.
*/
function employerInitiativeError(data: IFormData, attachments: IAttachmentEditor): string | undefined {
if (!data.EmployerChangeSchedule && !data.EmployerChangeSalary) return EMPLOYER_CHANGE_NOTHING_SELECTED_MESSAGE;
const hasJustification =
!!data.EmployerChangeJustification.trim() || hasAttachment(attachments, EMPLOYER_JUSTIFICATION_ATTACHMENT);
if (!hasJustification) return EMPLOYER_JUSTIFICATION_REQUIRED_MESSAGE;
const minDate = minEmployerChangeDate();
const dates = [
data.EmployerChangeSchedule ? data.EmployerChangeScheduleDate : '',
data.EmployerChangeSalary ? data.EmployerChangeSalaryDate : ''
];
if (dates.some((date) => date && date < minDate)) {
return `Дата изменений — не раньше ${formatIsoDate(minDate)}: работника уведомляют за ${EMPLOYER_CHANGE_NOTICE_DAYS} календарных дней.`;
}
return undefined;
}
@@ -28,13 +28,12 @@ export const CHANGE_TYPE_LABELS: Record<ChangeType, string> = {
'department-transfer': 'Перевод в другое подразделение',
'workplace-change': 'Изменение места работы (другая местность)',
'contract-extension': 'Продление срока трудового договора',
'schedule-change':
'Изменение режима / графика работы по соглашению сторон; неполное рабочее время по просьбе работника',
// В ТЗ ещё «; неполное рабочее время по просьбе работника» — сокращено по просьбе Тимура
// (неполное время — поля работника внутри вида).
'schedule-change': 'Изменение режима / графика работы по соглашению сторон',
'function-change': 'Изменение трудовой функции (без смены должности)',
// Самая длинная подпись (~225 символов) — близко к лимиту choice в SP (255), не удлинять.
'employer-initiative':
'Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, ' +
'изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)',
// В ТЗ — длинная формулировка с перечнем причин; сокращено по просьбе Тимура (причины — в поле «Причина» вида).
'employer-initiative': 'Изменение условий труда по инициативе работодателя',
'temporary-transfer': 'Временный перевод при производственной необходимости / простое'
};
@@ -4,6 +4,7 @@ import { spDateOnlyToIso } from '../../shared/dateUtils';
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
import { ChangeType, changeTypeFromLabel, initiatorRoleFromText } from './changeTypes';
import { allowanceUnitFromText } from './byType/Allowance/options';
import { DEFAULT_FIXED_PERCENT } from './byType/EmployerInitiative/salaryParts';
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
export interface ILoadedRequest {
@@ -141,7 +142,20 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
'ScheduleChangeReason',
'ScheduleChangeJustification',
'FunctionChangeDate',
'FunctionChangeReason'
'FunctionChangeReason',
'EmployerChangeReason',
'EmployerChangeReasonOther',
'EmployerChangeSchedule',
'EmployerChangeCurrentSchedule',
'EmployerChangeCurrentScheduleOther',
'EmployerChangeNewSchedule',
'EmployerChangeNewScheduleOther',
'EmployerChangeScheduleDate',
'EmployerChangeSalary',
'EmployerChangeNewSalary',
'EmployerChangeFixedPercent',
'EmployerChangeSalaryDate',
'EmployerChangeJustification'
)
.expand('DepartmentTransferReceivingHead')()) as Record<string, unknown>;
const receivingHead = item.DepartmentTransferReceivingHead as { EMail?: string } | undefined;
@@ -204,7 +218,22 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
ScheduleChangeReason: text(item.ScheduleChangeReason),
ScheduleChangeJustification: text(item.ScheduleChangeJustification),
FunctionChangeDate: spDateOnlyToIso(text(item.FunctionChangeDate)),
FunctionChangeReason: text(item.FunctionChangeReason)
FunctionChangeReason: text(item.FunctionChangeReason),
EmployerChangeReason: text(item.EmployerChangeReason),
EmployerChangeReasonOther: text(item.EmployerChangeReasonOther),
EmployerChangeSchedule: item.EmployerChangeSchedule === true,
EmployerChangeCurrentSchedule: text(item.EmployerChangeCurrentSchedule),
EmployerChangeCurrentScheduleOther: text(item.EmployerChangeCurrentScheduleOther),
EmployerChangeNewSchedule: text(item.EmployerChangeNewSchedule),
EmployerChangeNewScheduleOther: text(item.EmployerChangeNewScheduleOther),
EmployerChangeScheduleDate: spDateOnlyToIso(text(item.EmployerChangeScheduleDate)),
EmployerChangeSalary: item.EmployerChangeSalary === true,
// Суммы частей и переменную долю не читаем — они считаются (splitSalary).
EmployerChangeNewSalary: numberText(item.EmployerChangeNewSalary),
// Оклад не меняли (доли не сохранялись) — доля по умолчанию из ТЗ, 75 %.
EmployerChangeFixedPercent: numberText(item.EmployerChangeFixedPercent) || DEFAULT_FIXED_PERCENT,
EmployerChangeSalaryDate: spDateOnlyToIso(text(item.EmployerChangeSalaryDate)),
EmployerChangeJustification: text(item.EmployerChangeJustification)
};
return {
@@ -3,6 +3,7 @@ import { RequestTypeId } from '../../models/types';
import { IEmployeeFullBasePerson, IEmployeeFullBaseRecord } from '../../shared/dictionary';
import { ChangeType, InitiatorRole, INITIATOR_EMPLOYEE, INITIATOR_EMPLOYER } from './changeTypes';
import { AllowanceUnit, DEFAULT_ALLOWANCE_UNIT } from './byType/Allowance/options';
import { DEFAULT_FIXED_PERCENT } from './byType/EmployerInitiative/salaryParts';
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
/**
@@ -158,6 +159,32 @@ export interface IFormData {
FunctionChangeDate: string;
/** «В связи с ___ (текст)» из шаблона ТЗ — списка причин нет. */
FunctionChangeReason: string;
// ── Вид «Изменение условий труда по инициативе работодателя» (byType/EmployerInitiative).
// Только работодатель. Что меняется — флажки а) график / б) оклад (можно оба).
/** Причина — EMPLOYER_CHANGE_REASONS; при «Другое» — свой текст. */
EmployerChangeReason: string;
EmployerChangeReasonOther: string;
/** а) меняется график работы. */
EmployerChangeSchedule: boolean;
EmployerChangeCurrentSchedule: string;
EmployerChangeCurrentScheduleOther: string;
EmployerChangeNewSchedule: string;
EmployerChangeNewScheduleOther: string;
/** Не раньше чем через 15 дней (minEmployerChangeDate). ISO YYYY-MM-DD. */
EmployerChangeScheduleDate: string;
/** б) меняется должностной оклад. */
EmployerChangeSalary: boolean;
/**
* Новый оклад — «сырые» цифры (MoneyInput). Суммы частей и доля переменной в
* модели не храним — считаются (splitSalary); в SP пишутся при сохранении.
*/
EmployerChangeNewSalary: string;
/** Доля постоянной части, % (0–100, по умолчанию 75 из ТЗ); переменная — остаток до 100. */
EmployerChangeFixedPercent: string;
EmployerChangeSalaryDate: string;
/** Обоснование текстом — или документом (одно из двух обязательно при отправке). */
EmployerChangeJustification: string;
}
/** Обновление одного поля формы с сохранением типа значения. */
@@ -228,7 +255,20 @@ export const EMPTY_DATA: IFormData = {
ScheduleChangeReason: '',
ScheduleChangeJustification: '',
FunctionChangeDate: '',
FunctionChangeReason: ''
FunctionChangeReason: '',
EmployerChangeReason: '',
EmployerChangeReasonOther: '',
EmployerChangeSchedule: false,
EmployerChangeCurrentSchedule: '',
EmployerChangeCurrentScheduleOther: '',
EmployerChangeNewSchedule: '',
EmployerChangeNewScheduleOther: '',
EmployerChangeScheduleDate: '',
EmployerChangeSalary: false,
EmployerChangeNewSalary: '',
EmployerChangeFixedPercent: DEFAULT_FIXED_PERCENT,
EmployerChangeSalaryDate: '',
EmployerChangeJustification: ''
};
/** Плейсхолдер для поля выбора сотрудника. */
@@ -250,6 +290,10 @@ export const INITIATOR_HINT =
export const INITIATOR_REQUIRED_MESSAGE = 'Укажите инициатора изменения: работник или работодатель.';
/** Подпись поля текста у работника — вместо причин и обоснования (шаблон ТЗ «От работника: заявление»). */
export const EMPLOYEE_STATEMENT_LABEL = 'Текст заявления';
export const EMPLOYER_CHANGE_NOTHING_SELECTED_MESSAGE =
'Отметьте, что меняется: график работы и (или) должностной оклад.';
export const EMPLOYER_JUSTIFICATION_REQUIRED_MESSAGE =
'Укажите обоснование текстом или приложите документ-обоснование (приказ о реорганизации и т. п.).';
export const NEW_COMPANY_REQUIRED_MESSAGE = 'Выберите новую организацию — в какой организации будет новое место работы.';
export const RECEIVING_HEAD_REQUIRED_MESSAGE =
'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.';
@@ -20,9 +20,9 @@
"Перевод в другое подразделение",
"Изменение места работы (другая местность)",
"Продление срока трудового договора",
"Изменение режима / графика работы по соглашению сторон; неполное рабочее время по просьбе работника",
"Изменение режима / графика работы по соглашению сторон",
"Изменение трудовой функции (без смены должности)",
"Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)",
"Изменение условий труда по инициативе работодателя",
"Временный перевод при производственной необходимости / простое"
] },
{ "internalName": "ChangeInitiator", "title": "Инициатор изменения", "type": "Choice", "choices": ["Работник", "Работодатель"] },
@@ -109,6 +109,29 @@
{ "internalName": "ScheduleChangeJustification", "title": "Режим работы: обоснование / текст заявления", "type": "Note" },
{ "internalName": "FunctionChangeDate", "title": "Трудовая функция: дата изменения", "type": "DateTime", "dateOnly": true },
{ "internalName": "FunctionChangeReason", "title": "Трудовая функция: в связи с", "type": "Note" }
{ "internalName": "FunctionChangeReason", "title": "Трудовая функция: в связи с", "type": "Note" },
{ "internalName": "EmployerChangeReason", "title": "Инициатива работодателя: причина", "type": "Choice", "choices": [
"Экономические причины",
"Технологические причины",
"Изменения в организации",
"Сокращение объёма работ",
"Другое"
] },
{ "internalName": "EmployerChangeReasonOther", "title": "Инициатива работодателя: причина (другое)", "type": "Text" },
{ "internalName": "EmployerChangeSchedule", "title": "Инициатива работодателя: меняется график", "type": "Boolean" },
{ "internalName": "EmployerChangeCurrentSchedule", "title": "Инициатива работодателя: текущий график", "type": "Text" },
{ "internalName": "EmployerChangeCurrentScheduleOther", "title": "Инициатива работодателя: текущий график (другое)", "type": "Text" },
{ "internalName": "EmployerChangeNewSchedule", "title": "Инициатива работодателя: новый график", "type": "Text" },
{ "internalName": "EmployerChangeNewScheduleOther", "title": "Инициатива работодателя: новый график (другое)", "type": "Text" },
{ "internalName": "EmployerChangeScheduleDate", "title": "Инициатива работодателя: дата изменения графика", "type": "DateTime", "dateOnly": true },
{ "internalName": "EmployerChangeSalary", "title": "Инициатива работодателя: меняется оклад", "type": "Boolean" },
{ "internalName": "EmployerChangeNewSalary", "title": "Инициатива работодателя: новый оклад", "type": "Number", "decimals": 0 },
{ "internalName": "EmployerChangeFixedPercent", "title": "Инициатива работодателя: постоянная часть, %", "type": "Number", "decimals": 2 },
{ "internalName": "EmployerChangeVariablePercent", "title": "Инициатива работодателя: переменная часть, %", "type": "Number", "decimals": 2 },
{ "internalName": "EmployerChangeFixedPart", "title": "Инициатива работодателя: постоянная часть, ₸", "type": "Number", "decimals": 0 },
{ "internalName": "EmployerChangeVariablePart", "title": "Инициатива работодателя: переменная часть, ₸", "type": "Number", "decimals": 0 },
{ "internalName": "EmployerChangeSalaryDate", "title": "Инициатива работодателя: дата изменения оклада", "type": "DateTime", "dateOnly": true },
{ "internalName": "EmployerChangeJustification", "title": "Инициатива работодателя: обоснование", "type": "Note" }
]
}
@@ -4,6 +4,7 @@ import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
import { ensureEmployee } from '../../shared/employee';
import { CHANGE_TYPE_LABELS, INITIATOR_EMPLOYEE, hasInitiatorChoice } from './changeTypes';
import { FORM_ID, IFormData } from './model';
import { splitSalary } from './byType/EmployerInitiative/salaryParts';
/**
* Экземпляр сервиса списка согласования этой формы — тонкая обёртка над
@@ -224,6 +225,38 @@ function keepOrClear(
return result;
}
/**
* Колонки вида «Инициатива работодателя»: общие + группа графика (флажок а) +
* группа оклада (флажок б). Переменная доля и суммы частей считаются здесь же (splitSalary).
*/
function employerInitiativeColumns(data: IFormData): Record<string, unknown> {
const isSelected = data.ChangeTypes.includes('employer-initiative');
const parts = splitSalary(data.EmployerChangeNewSalary, data.EmployerChangeFixedPercent);
const common = keepOrClear(isSelected, {
EmployerChangeReason: data.EmployerChangeReason,
EmployerChangeReasonOther: data.EmployerChangeReasonOther.trim(),
EmployerChangeSchedule: data.EmployerChangeSchedule,
EmployerChangeSalary: data.EmployerChangeSalary,
EmployerChangeJustification: data.EmployerChangeJustification
});
const scheduleFields = keepOrClear(isSelected && data.EmployerChangeSchedule, {
EmployerChangeCurrentSchedule: data.EmployerChangeCurrentSchedule,
EmployerChangeCurrentScheduleOther: data.EmployerChangeCurrentScheduleOther.trim(),
EmployerChangeNewSchedule: data.EmployerChangeNewSchedule,
EmployerChangeNewScheduleOther: data.EmployerChangeNewScheduleOther.trim(),
EmployerChangeScheduleDate: data.EmployerChangeScheduleDate
});
const salaryFields = keepOrClear(isSelected && data.EmployerChangeSalary, {
EmployerChangeNewSalary: numberColumn(data.EmployerChangeNewSalary),
EmployerChangeFixedPercent: numberColumn(data.EmployerChangeFixedPercent),
EmployerChangeVariablePercent: parts?.variablePercent,
EmployerChangeFixedPart: parts?.fixed,
EmployerChangeVariablePart: parts?.variable,
EmployerChangeSalaryDate: data.EmployerChangeSalaryDate
});
return { ...common, ...scheduleFields, ...salaryFields };
}
/** Поля элемента списка данных из модели формы. */
function buildItem(data: IFormData, status: string, receivingHeadId: number | undefined): Record<string, unknown> {
return {
@@ -242,7 +275,8 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
...keepOrClear(data.ChangeTypes.includes('function-change'), {
FunctionChangeDate: data.FunctionChangeDate,
FunctionChangeReason: data.FunctionChangeReason
})
}),
...employerInitiativeColumns(data)
};
}