feat: поля вида «Инициатива работодателя» и короткие названия видов
Девятый вид: причина (из ТЗ + «Другое»), что меняется — график работы и/или оклад; оклад вводится один, долю постоянной части можно задать (по умолчанию 75 % из ТЗ), переменная — остаток до 100 %, суммы считаются сами; даты не раньше чем через 15 дней (срок уведомления работника); обоснование — текстом или документом. Длинные названия видов 7 и 9 сокращены, а дублирующий выбранный вид заголовок над полями убран у всех видов. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,8 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
|||||||
| Продление срока ТД | — | **пропущен** (2026-10-29): по ТЗ это автоматический запрос ОК за месяц до окончания срочного ТД (маршрут В), а не обычная заявка — решать отдельно |
|
| Продление срока ТД | — | **пропущен** (2026-10-29): по ТЗ это автоматический запрос ОК за месяц до окончания срочного ТД (маршрут В), а не обычная заявка — решать отдельно |
|
||||||
| Изменение режима / графика работы | `ScheduleChange` | по ролям строго по ТЗ. Общее: текущий график + текущее время работы (списки как в подборе/приёме, «Другое» → текст; `OptionWithOtherField`, наборы — `byType/workSchedule.ts`), дата начала, вложение «Новый график» (необязательно). **Работник:** неполный рабочий день (часов) / неполная рабочая неделя (дней) — одно или оба, текст заявления. **Работодатель:** новый режим (вахтовый / сменный / иной + описание), новые график и время работы, причина (специфика объекта / смещение сроков проекта — только из ТЗ), обоснование. Поля другой роли при сохранении очищаются |
|
| Изменение режима / графика работы | `ScheduleChange` | по ролям строго по ТЗ. Общее: текущий график + текущее время работы (списки как в подборе/приёме, «Другое» → текст; `OptionWithOtherField`, наборы — `byType/workSchedule.ts`), дата начала, вложение «Новый график» (необязательно). **Работник:** неполный рабочий день (часов) / неполная рабочая неделя (дней) — одно или оба, текст заявления. **Работодатель:** новый режим (вахтовый / сменный / иной + описание), новые график и время работы, причина (специфика объекта / смещение сроков проекта — только из ТЗ), обоснование. Поля другой роли при сохранении очищаются |
|
||||||
| Изменение трудовой функции (без смены должности) | `FunctionChange` | ровно по шаблону ТЗ: дата изменения, «В связи с» (свободный текст — списка причин в ТЗ нет), вложение **«Новая должностная инструкция»** — обязательно. Инициатор — только руководитель |
|
| Изменение трудовой функции (без смены должности) | `FunctionChange` | ровно по шаблону ТЗ: дата изменения, «В связи с» (свободный текст — списка причин в ТЗ нет), вложение **«Новая должностная инструкция»** — обязательно. Инициатор — только руководитель |
|
||||||
| Остальные 2 | — | ещё не разобраны с кадрами |
|
| Изменение условий труда по инициативе работодателя | `EmployerInitiative` | причина (из названия вида в ТЗ: экономические / технологические / изменения в организации / сокращение объёма работ + «Другое» со своим текстом); «Что меняется» — флажки а) график работы, б) должностной оклад (можно оба, при отправке — хотя бы один); а) текущий → новый график + дата; б) **один новый оклад + доля постоянной части в %** (по умолчанию 75 из ТЗ, 0–100, можно дробную), переменная доля = остаток до 100 %, суммы обеих частей в ₸ считаются сами (`salaryParts.ts`, превысить 100 % нельзя; в SP пишутся при сохранении) + дата; даты **не раньше +15 дней** (уведомление по ТЗ: блок в календаре + проверка при отправке); обоснование — **текстом ИЛИ документом**, одно из двух обязательно при отправке |
|
||||||
|
| Остальные 1 | — | ещё не разобран с кадрами |
|
||||||
|
|
||||||
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
|
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
|
||||||
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
||||||
|
|||||||
+4
-5
@@ -1,8 +1,7 @@
|
|||||||
import * as React from 'react';
|
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 { useFormReadOnly } from '@tnsu/ui-kit';
|
||||||
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { ALLOWANCE_REASONS, ATTESTATION_ATTACHMENT, requiresAttestation } from './options';
|
import { ALLOWANCE_REASONS, ATTESTATION_ATTACHMENT, requiresAttestation } from './options';
|
||||||
import { UnitSwitch } from './UnitSwitch';
|
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 }) => {
|
export const AllowanceSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
|
||||||
const readOnly = useFormReadOnly();
|
const readOnly = useFormReadOnly();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title={CHANGE_TYPE_LABELS.allowance} icon="calculator">
|
<FormGrid>
|
||||||
<FormField label="Основание" htmlFor="AllowanceReason" full>
|
<FormField label="Основание" htmlFor="AllowanceReason" full>
|
||||||
<Select
|
<Select
|
||||||
id="AllowanceReason"
|
id="AllowanceReason"
|
||||||
@@ -99,6 +98,6 @@ export const AllowanceSection: React.FC<ISectionProps> = ({ data, set, attachmen
|
|||||||
required
|
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 { WorkplaceChangeSection } from './WorkplaceChange/WorkplaceChangeSection';
|
||||||
import { ScheduleChangeSection } from './ScheduleChange/ScheduleChangeSection';
|
import { ScheduleChangeSection } from './ScheduleChange/ScheduleChangeSection';
|
||||||
import { FunctionChangeSection } from './FunctionChange/FunctionChangeSection';
|
import { FunctionChangeSection } from './FunctionChange/FunctionChangeSection';
|
||||||
|
import { EmployerInitiativeSection } from './EmployerInitiative/EmployerInitiativeSection';
|
||||||
|
import styles from './ChangeTypeSections.module.scss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
||||||
@@ -21,15 +23,23 @@ const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
|
|||||||
'department-transfer': DepartmentTransferSection,
|
'department-transfer': DepartmentTransferSection,
|
||||||
'workplace-change': WorkplaceChangeSection,
|
'workplace-change': WorkplaceChangeSection,
|
||||||
'schedule-change': ScheduleChangeSection,
|
'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) => (
|
export const ChangeTypeSections: React.FC<ISectionProps> = (props) => (
|
||||||
<>
|
<>
|
||||||
{props.data.ChangeTypes.map((type) => {
|
{props.data.ChangeTypes.map((type) => {
|
||||||
const Section = SECTION_BY_TYPE[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;
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-5
@@ -1,8 +1,7 @@
|
|||||||
import * as React from 'react';
|
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 { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
import { DepartmentPicker } from '../../../../shared/dictionary';
|
import { DepartmentPicker } from '../../../../shared/dictionary';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { DEPARTMENT_TRANSFER_REASONS } from './options';
|
import { DEPARTMENT_TRANSFER_REASONS } from './options';
|
||||||
|
|
||||||
@@ -12,10 +11,10 @@ import { DEPARTMENT_TRANSFER_REASONS } from './options';
|
|||||||
* принимающий руководитель. Его согласие по ТЗ — шаг маршрута: форма сама ставит
|
* принимающий руководитель. Его согласие по ТЗ — шаг маршрута: форма сама ставит
|
||||||
* его первым шагом согласования (см. receivingHead.ts). Обязателен при отправке.
|
* его первым шагом согласования (см. receivingHead.ts). Обязателен при отправке.
|
||||||
*
|
*
|
||||||
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
|
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
|
||||||
*/
|
*/
|
||||||
export const DepartmentTransferSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
export const DepartmentTransferSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||||
<CollapseSection title={CHANGE_TYPE_LABELS['department-transfer']} icon="people">
|
<FormGrid>
|
||||||
<FormField label="Новое подразделение" htmlFor="DepartmentTransferNewDepartment">
|
<FormField label="Новое подразделение" htmlFor="DepartmentTransferNewDepartment">
|
||||||
<DepartmentPicker
|
<DepartmentPicker
|
||||||
context={context}
|
context={context}
|
||||||
@@ -59,5 +58,5 @@ export const DepartmentTransferSection: React.FC<ISectionProps> = ({ data, set,
|
|||||||
onChange: (v) => set('DepartmentTransferJustification', v)
|
onChange: (v) => set('DepartmentTransferJustification', v)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CollapseSection>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
|
|||||||
+15
@@ -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;
|
||||||
|
}
|
||||||
+179
@@ -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 = 'Документ-обоснование';
|
||||||
+43
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
+50
@@ -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 };
|
||||||
|
}
|
||||||
+4
-5
@@ -1,8 +1,7 @@
|
|||||||
import * as React from 'react';
|
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 { useFormReadOnly } from '@tnsu/ui-kit';
|
||||||
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './options';
|
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 }) => {
|
export const FunctionChangeSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
|
||||||
const readOnly = useFormReadOnly();
|
const readOnly = useFormReadOnly();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title={CHANGE_TYPE_LABELS['function-change']} icon="document">
|
<FormGrid>
|
||||||
<FormField label="Дата изменения" htmlFor="FunctionChangeDate">
|
<FormField label="Дата изменения" htmlFor="FunctionChangeDate">
|
||||||
<TextInput
|
<TextInput
|
||||||
id="FunctionChangeDate"
|
id="FunctionChangeDate"
|
||||||
@@ -45,6 +44,6 @@ export const FunctionChangeSection: React.FC<ISectionProps> = ({ data, set, atta
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CollapseSection>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface Props {
|
|||||||
* Окончание секции вида, где по ТЗ шаблон зависит от инициатора:
|
* Окончание секции вида, где по ТЗ шаблон зависит от инициатора:
|
||||||
* - работник — «заявление (текст)»: только «Текст заявления», без списка причин;
|
* - работник — «заявление (текст)»: только «Текст заявления», без списка причин;
|
||||||
* - работодатель (и пока инициатор не выбран) — «в связи с: а) б) в)»: причина + обоснование.
|
* - работодатель (и пока инициатор не выбран) — «в связи с: а) б) в)»: причина + обоснование.
|
||||||
* Поля кладутся прямо в сетку CollapseSection (без обёртки) — как и остальные поля вида.
|
* Поля кладутся прямо в сетку FormGrid секции (без обёртки) — как и остальные поля вида.
|
||||||
*/
|
*/
|
||||||
export const InitiatorReasonFields: React.FC<Props> = ({ initiator, reason, justification }) => {
|
export const InitiatorReasonFields: React.FC<Props> = ({ initiator, reason, justification }) => {
|
||||||
const isEmployee = initiator === INITIATOR_EMPLOYEE;
|
const isEmployee = initiator === INITIATOR_EMPLOYEE;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface Props {
|
|||||||
/**
|
/**
|
||||||
* Выбор из списка + ручной ввод при «Другое» (как график/время работы в подборе
|
* Выбор из списка + ручной ввод при «Другое» (как график/время работы в подборе
|
||||||
* и приёме). Выбрали не «Другое» — ручной текст очищается, чтобы не сохранить
|
* и приёме). Выбрали не «Другое» — ручной текст очищается, чтобы не сохранить
|
||||||
* скрытое значение. Поля кладутся прямо в сетку CollapseSection.
|
* скрытое значение. Поля кладутся прямо в сетку FormGrid секции.
|
||||||
*/
|
*/
|
||||||
export const OptionWithOtherField: React.FC<Props> = ({
|
export const OptionWithOtherField: React.FC<Props> = ({
|
||||||
label,
|
label,
|
||||||
|
|||||||
+4
-5
@@ -1,10 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CollapseSection, FormField, TextInput } from '../../../../shared/ui';
|
import { FormGrid, FormField, TextInput } from '../../../../shared/ui';
|
||||||
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
import { useFormReadOnly } from '@tnsu/ui-kit';
|
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||||
import { PositionPicker } from '../../../../shared/dictionary';
|
import { PositionPicker } from '../../../../shared/dictionary';
|
||||||
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { POSITION_TRANSFER_REASONS, JOB_DESCRIPTION_ATTACHMENT } from './options';
|
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 }) => {
|
export const PositionTransferSection: React.FC<ISectionProps> = ({ data, set, context, attachments }) => {
|
||||||
const readOnly = useFormReadOnly();
|
const readOnly = useFormReadOnly();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title={CHANGE_TYPE_LABELS['position-transfer']} icon="people">
|
<FormGrid>
|
||||||
<FormField label="Новая должность" htmlFor="PositionTransferNewPosition">
|
<FormField label="Новая должность" htmlFor="PositionTransferNewPosition">
|
||||||
<PositionPicker
|
<PositionPicker
|
||||||
context={context}
|
context={context}
|
||||||
@@ -62,6 +61,6 @@ export const PositionTransferSection: React.FC<ISectionProps> = ({ data, set, co
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CollapseSection>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-6
@@ -1,6 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CollapseSection, FormField, MoneyInput, TextInput, TextArea, Select } from '../../../../shared/ui';
|
import { FormGrid, FormField, MoneyInput, TextInput, TextArea, Select } from '../../../../shared/ui';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { SALARY_INCREASE_REASONS } from './reasons';
|
import { SALARY_INCREASE_REASONS } from './reasons';
|
||||||
|
|
||||||
@@ -9,11 +8,10 @@ import { SALARY_INCREASE_REASONS } from './reasons';
|
|||||||
* кадров: было/стало, дата, причина (а/б/в) и обоснование. Все необязательные,
|
* кадров: было/стало, дата, причина (а/б/в) и обоснование. Все необязательные,
|
||||||
* без ограничений — по решению от 2026-10-28.
|
* без ограничений — по решению от 2026-10-28.
|
||||||
*
|
*
|
||||||
* Поля — прямо в CollapseSection, без FormGrid: тело секции само является
|
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
|
||||||
* двухколоночной сеткой, вложенная FormGrid сжала бы всё в одну её ячейку.
|
|
||||||
*/
|
*/
|
||||||
export const SalaryIncreaseSection: React.FC<ISectionProps> = ({ data, set }) => (
|
export const SalaryIncreaseSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||||
<CollapseSection title={CHANGE_TYPE_LABELS['salary-increase']} icon="calculator">
|
<FormGrid>
|
||||||
<FormField label="Текущий оклад, ₸" htmlFor="SalaryIncreaseCurrent">
|
<FormField label="Текущий оклад, ₸" htmlFor="SalaryIncreaseCurrent">
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
id="SalaryIncreaseCurrent"
|
id="SalaryIncreaseCurrent"
|
||||||
@@ -52,5 +50,5 @@ export const SalaryIncreaseSection: React.FC<ISectionProps> = ({ data, set }) =>
|
|||||||
onChange={(v) => set('SalaryIncreaseJustification', v)}
|
onChange={(v) => set('SalaryIncreaseJustification', v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</CollapseSection>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
|
|||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react';
|
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 { useFormReadOnly } from '@tnsu/ui-kit';
|
||||||
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
||||||
import { CHANGE_TYPE_LABELS, INITIATOR_EMPLOYEE } from '../../changeTypes';
|
import { INITIATOR_EMPLOYEE } from '../../changeTypes';
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
import { OptionWithOtherField } from '../OptionWithOtherField';
|
import { OptionWithOtherField } from '../OptionWithOtherField';
|
||||||
@@ -21,14 +21,14 @@ import {
|
|||||||
* (часов в день / дней в неделю) и текст заявления; работодатель — новый режим,
|
* (часов в день / дней в неделю) и текст заявления; работодатель — новый режим,
|
||||||
* новые график и время работы, причина и обоснование. Все поля необязательные.
|
* новые график и время работы, причина и обоснование. Все поля необязательные.
|
||||||
*
|
*
|
||||||
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
|
* Поля — в FormGrid без заголовка: вид уже выбран выше, заголовок его только дублировал.
|
||||||
*/
|
*/
|
||||||
export const ScheduleChangeSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
|
export const ScheduleChangeSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
|
||||||
const readOnly = useFormReadOnly();
|
const readOnly = useFormReadOnly();
|
||||||
const isEmployee = data.ChangeInitiator === INITIATOR_EMPLOYEE;
|
const isEmployee = data.ChangeInitiator === INITIATOR_EMPLOYEE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title={CHANGE_TYPE_LABELS['schedule-change']} icon="calendar">
|
<FormGrid>
|
||||||
<OptionWithOtherField
|
<OptionWithOtherField
|
||||||
label="Текущий график работы"
|
label="Текущий график работы"
|
||||||
id="ScheduleChangeCurrentSchedule"
|
id="ScheduleChangeCurrentSchedule"
|
||||||
@@ -148,6 +148,6 @@ export const ScheduleChangeSection: React.FC<ISectionProps> = ({ data, set, atta
|
|||||||
category={NEW_SCHEDULE_ATTACHMENT}
|
category={NEW_SCHEDULE_ATTACHMENT}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CollapseSection>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-5
@@ -1,7 +1,6 @@
|
|||||||
import * as React from 'react';
|
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 { ProjectPicker } from '../../../../shared/dictionary';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { WORKPLACE_CHANGE_REASONS } from './options';
|
import { WORKPLACE_CHANGE_REASONS } from './options';
|
||||||
import { WORK_SCHEDULE_OPTIONS } from '../workSchedule';
|
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 }) => {
|
export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, context }) => {
|
||||||
const companies = useCompanyOptions(context);
|
const companies = useCompanyOptions(context);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title={CHANGE_TYPE_LABELS['workplace-change']} icon="vehicle">
|
<FormGrid>
|
||||||
<FormField label="Текущая организация" htmlFor="WorkplaceChangeCurrentCompany">
|
<FormField label="Текущая организация" htmlFor="WorkplaceChangeCurrentCompany">
|
||||||
{/* Та же, что выбрана в «Организации» сотрудника, — второй раз не вводим. */}
|
{/* Та же, что выбрана в «Организации» сотрудника, — второй раз не вводим. */}
|
||||||
<TextInput id="WorkplaceChangeCurrentCompany" value={data.Employee?.company || ''} onChange={() => undefined} readOnly />
|
<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)
|
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 { JOB_DESCRIPTION_ATTACHMENT } from './PositionTransfer/options';
|
||||||
import { NEW_SCHEDULE_ATTACHMENT } from './ScheduleChange/options';
|
import { NEW_SCHEDULE_ATTACHMENT } from './ScheduleChange/options';
|
||||||
import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './FunctionChange/options';
|
import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './FunctionChange/options';
|
||||||
|
import { EMPLOYER_JUSTIFICATION_ATTACHMENT } from './EmployerInitiative/options';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
|
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
|
||||||
@@ -28,6 +29,10 @@ export function activeAttachmentSlots(data: IFormData): IAttachmentSlot[] {
|
|||||||
if (data.ChangeTypes.includes('function-change')) {
|
if (data.ChangeTypes.includes('function-change')) {
|
||||||
slots.push({ category: NEW_JOB_DESCRIPTION_ATTACHMENT, required: true });
|
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')) {
|
if (data.ChangeTypes.includes('schedule-change')) {
|
||||||
// «Можно приложить документ» — необязательно.
|
// «Можно приложить документ» — необязательно.
|
||||||
slots.push({ category: NEW_SCHEDULE_ATTACHMENT, required: false });
|
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) : []);
|
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[] {
|
export function missingRequiredAttachments(data: IFormData, editor: IAttachmentEditor): string[] {
|
||||||
return activeAttachmentSlots(data)
|
return activeAttachmentSlots(data)
|
||||||
.filter((slot) => slot.required)
|
.filter((slot) => slot.required && !hasAttachment(editor, slot.category))
|
||||||
.filter((slot) => {
|
|
||||||
const uploaded = attachmentsOfCategory(editor.items, slot.category).length;
|
|
||||||
const selected = (editor.pending[slot.category] || []).length;
|
|
||||||
return uploaded + selected === 0;
|
|
||||||
})
|
|
||||||
.map((slot) => slot.category);
|
.map((slot) => slot.category);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { IAttachmentEditor } from '../../../shared/attachments/useAttachmentEditor';
|
import { IAttachmentEditor } from '../../../shared/attachments/useAttachmentEditor';
|
||||||
|
import { formatIsoDate } from '../../../shared/dateUtils';
|
||||||
import {
|
import {
|
||||||
IFormData,
|
IFormData,
|
||||||
INITIATOR_REQUIRED_MESSAGE,
|
INITIATOR_REQUIRED_MESSAGE,
|
||||||
NEW_COMPANY_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';
|
} from '../model';
|
||||||
import { hasInitiatorChoice } from '../changeTypes';
|
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) {
|
if (data.ChangeTypes.includes('workplace-change') && !data.WorkplaceChangeNewCompanyId) {
|
||||||
return NEW_COMPANY_REQUIRED_MESSAGE;
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,12 @@ export const CHANGE_TYPE_LABELS: Record<ChangeType, string> = {
|
|||||||
'department-transfer': 'Перевод в другое подразделение',
|
'department-transfer': 'Перевод в другое подразделение',
|
||||||
'workplace-change': 'Изменение места работы (другая местность)',
|
'workplace-change': 'Изменение места работы (другая местность)',
|
||||||
'contract-extension': 'Продление срока трудового договора',
|
'contract-extension': 'Продление срока трудового договора',
|
||||||
'schedule-change':
|
// В ТЗ ещё «; неполное рабочее время по просьбе работника» — сокращено по просьбе Тимура
|
||||||
'Изменение режима / графика работы по соглашению сторон; неполное рабочее время по просьбе работника',
|
// (неполное время — поля работника внутри вида).
|
||||||
|
'schedule-change': 'Изменение режима / графика работы по соглашению сторон',
|
||||||
'function-change': 'Изменение трудовой функции (без смены должности)',
|
'function-change': 'Изменение трудовой функции (без смены должности)',
|
||||||
// Самая длинная подпись (~225 символов) — близко к лимиту choice в SP (255), не удлинять.
|
// В ТЗ — длинная формулировка с перечнем причин; сокращено по просьбе Тимура (причины — в поле «Причина» вида).
|
||||||
'employer-initiative':
|
'employer-initiative': 'Изменение условий труда по инициативе работодателя',
|
||||||
'Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, ' +
|
|
||||||
'изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)',
|
|
||||||
'temporary-transfer': 'Временный перевод при производственной необходимости / простое'
|
'temporary-transfer': 'Временный перевод при производственной необходимости / простое'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { spDateOnlyToIso } from '../../shared/dateUtils';
|
|||||||
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||||
import { ChangeType, changeTypeFromLabel, initiatorRoleFromText } from './changeTypes';
|
import { ChangeType, changeTypeFromLabel, initiatorRoleFromText } from './changeTypes';
|
||||||
import { allowanceUnitFromText } from './byType/Allowance/options';
|
import { allowanceUnitFromText } from './byType/Allowance/options';
|
||||||
|
import { DEFAULT_FIXED_PERCENT } from './byType/EmployerInitiative/salaryParts';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -141,7 +142,20 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
'ScheduleChangeReason',
|
'ScheduleChangeReason',
|
||||||
'ScheduleChangeJustification',
|
'ScheduleChangeJustification',
|
||||||
'FunctionChangeDate',
|
'FunctionChangeDate',
|
||||||
'FunctionChangeReason'
|
'FunctionChangeReason',
|
||||||
|
'EmployerChangeReason',
|
||||||
|
'EmployerChangeReasonOther',
|
||||||
|
'EmployerChangeSchedule',
|
||||||
|
'EmployerChangeCurrentSchedule',
|
||||||
|
'EmployerChangeCurrentScheduleOther',
|
||||||
|
'EmployerChangeNewSchedule',
|
||||||
|
'EmployerChangeNewScheduleOther',
|
||||||
|
'EmployerChangeScheduleDate',
|
||||||
|
'EmployerChangeSalary',
|
||||||
|
'EmployerChangeNewSalary',
|
||||||
|
'EmployerChangeFixedPercent',
|
||||||
|
'EmployerChangeSalaryDate',
|
||||||
|
'EmployerChangeJustification'
|
||||||
)
|
)
|
||||||
.expand('DepartmentTransferReceivingHead')()) as Record<string, unknown>;
|
.expand('DepartmentTransferReceivingHead')()) as Record<string, unknown>;
|
||||||
const receivingHead = item.DepartmentTransferReceivingHead as { EMail?: string } | undefined;
|
const receivingHead = item.DepartmentTransferReceivingHead as { EMail?: string } | undefined;
|
||||||
@@ -204,7 +218,22 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
ScheduleChangeReason: text(item.ScheduleChangeReason),
|
ScheduleChangeReason: text(item.ScheduleChangeReason),
|
||||||
ScheduleChangeJustification: text(item.ScheduleChangeJustification),
|
ScheduleChangeJustification: text(item.ScheduleChangeJustification),
|
||||||
FunctionChangeDate: spDateOnlyToIso(text(item.FunctionChangeDate)),
|
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 {
|
return {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { RequestTypeId } from '../../models/types';
|
|||||||
import { IEmployeeFullBasePerson, IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
import { IEmployeeFullBasePerson, IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||||
import { ChangeType, InitiatorRole, INITIATOR_EMPLOYEE, INITIATOR_EMPLOYER } from './changeTypes';
|
import { ChangeType, InitiatorRole, INITIATOR_EMPLOYEE, INITIATOR_EMPLOYER } from './changeTypes';
|
||||||
import { AllowanceUnit, DEFAULT_ALLOWANCE_UNIT } from './byType/Allowance/options';
|
import { AllowanceUnit, DEFAULT_ALLOWANCE_UNIT } from './byType/Allowance/options';
|
||||||
|
import { DEFAULT_FIXED_PERCENT } from './byType/EmployerInitiative/salaryParts';
|
||||||
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,6 +159,32 @@ export interface IFormData {
|
|||||||
FunctionChangeDate: string;
|
FunctionChangeDate: string;
|
||||||
/** «В связи с ___ (текст)» из шаблона ТЗ — списка причин нет. */
|
/** «В связи с ___ (текст)» из шаблона ТЗ — списка причин нет. */
|
||||||
FunctionChangeReason: 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: '',
|
ScheduleChangeReason: '',
|
||||||
ScheduleChangeJustification: '',
|
ScheduleChangeJustification: '',
|
||||||
FunctionChangeDate: '',
|
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 INITIATOR_REQUIRED_MESSAGE = 'Укажите инициатора изменения: работник или работодатель.';
|
||||||
/** Подпись поля текста у работника — вместо причин и обоснования (шаблон ТЗ «От работника: заявление»). */
|
/** Подпись поля текста у работника — вместо причин и обоснования (шаблон ТЗ «От работника: заявление»). */
|
||||||
export const EMPLOYEE_STATEMENT_LABEL = 'Текст заявления';
|
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 NEW_COMPANY_REQUIRED_MESSAGE = 'Выберите новую организацию — в какой организации будет новое место работы.';
|
||||||
export const RECEIVING_HEAD_REQUIRED_MESSAGE =
|
export const RECEIVING_HEAD_REQUIRED_MESSAGE =
|
||||||
'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.';
|
'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.';
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
"Перевод в другое подразделение",
|
"Перевод в другое подразделение",
|
||||||
"Изменение места работы (другая местность)",
|
"Изменение места работы (другая местность)",
|
||||||
"Продление срока трудового договора",
|
"Продление срока трудового договора",
|
||||||
"Изменение режима / графика работы по соглашению сторон; неполное рабочее время по просьбе работника",
|
"Изменение режима / графика работы по соглашению сторон",
|
||||||
"Изменение трудовой функции (без смены должности)",
|
"Изменение трудовой функции (без смены должности)",
|
||||||
"Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)",
|
"Изменение условий труда по инициативе работодателя",
|
||||||
"Временный перевод при производственной необходимости / простое"
|
"Временный перевод при производственной необходимости / простое"
|
||||||
] },
|
] },
|
||||||
{ "internalName": "ChangeInitiator", "title": "Инициатор изменения", "type": "Choice", "choices": ["Работник", "Работодатель"] },
|
{ "internalName": "ChangeInitiator", "title": "Инициатор изменения", "type": "Choice", "choices": ["Работник", "Работодатель"] },
|
||||||
@@ -109,6 +109,29 @@
|
|||||||
{ "internalName": "ScheduleChangeJustification", "title": "Режим работы: обоснование / текст заявления", "type": "Note" },
|
{ "internalName": "ScheduleChangeJustification", "title": "Режим работы: обоснование / текст заявления", "type": "Note" },
|
||||||
|
|
||||||
{ "internalName": "FunctionChangeDate", "title": "Трудовая функция: дата изменения", "type": "DateTime", "dateOnly": true },
|
{ "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 { ensureEmployee } from '../../shared/employee';
|
||||||
import { CHANGE_TYPE_LABELS, INITIATOR_EMPLOYEE, hasInitiatorChoice } from './changeTypes';
|
import { CHANGE_TYPE_LABELS, INITIATOR_EMPLOYEE, hasInitiatorChoice } from './changeTypes';
|
||||||
import { FORM_ID, IFormData } from './model';
|
import { FORM_ID, IFormData } from './model';
|
||||||
|
import { splitSalary } from './byType/EmployerInitiative/salaryParts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Экземпляр сервиса списка согласования этой формы — тонкая обёртка над
|
* Экземпляр сервиса списка согласования этой формы — тонкая обёртка над
|
||||||
@@ -224,6 +225,38 @@ function keepOrClear(
|
|||||||
return result;
|
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> {
|
function buildItem(data: IFormData, status: string, receivingHeadId: number | undefined): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
@@ -242,7 +275,8 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
|
|||||||
...keepOrClear(data.ChangeTypes.includes('function-change'), {
|
...keepOrClear(data.ChangeTypes.includes('function-change'), {
|
||||||
FunctionChangeDate: data.FunctionChangeDate,
|
FunctionChangeDate: data.FunctionChangeDate,
|
||||||
FunctionChangeReason: data.FunctionChangeReason
|
FunctionChangeReason: data.FunctionChangeReason
|
||||||
})
|
}),
|
||||||
|
...employerInitiativeColumns(data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user