feat: поля вида «Изменение режима / графика работы» в изменении условий ТД
Седьмой вид, по ролям строго по шаблонам ТЗ. Работник просит неполный рабочий день / неполную рабочую неделю и пишет заявление; работодатель переводит на вахтовый, сменный или иной режим с новым графиком и временем работы и указывает причину из ТЗ. Текущий режим и дата — у обоих, документ «Новый график» необязателен. Списки графика и времени работы и поле «список + Другое» стали общими для видов формы. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,9 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
|||||||
| Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно |
|
| Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно |
|
||||||
| Перевод в другое подразделение | `DepartmentTransfer` | новое подразделение (`DepartmentPicker`, имя + id), дата перевода, причина (усиление подразделения / перераспределение функций / вакансия / иное), обоснование, **принимающий руководитель** (People, обязателен при отправке) — см. ниже |
|
| Перевод в другое подразделение | `DepartmentTransfer` | новое подразделение (`DepartmentPicker`, имя + id), дата перевода, причина (усиление подразделения / перераспределение функций / вакансия / иное), обоснование, **принимающий руководитель** (People, обязателен при отправке) — см. ниже |
|
||||||
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
|
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
|
||||||
| Остальные 5 | — | ещё не разобраны с кадрами |
|
| Продление срока ТД | — | **пропущен** (2026-10-29): по ТЗ это автоматический запрос ОК за месяц до окончания срочного ТД (маршрут В), а не обычная заявка — решать отдельно |
|
||||||
|
| Изменение режима / графика работы | `ScheduleChange` | по ролям строго по ТЗ. Общее: текущий график + текущее время работы (списки как в подборе/приёме, «Другое» → текст; `OptionWithOtherField`, наборы — `byType/workSchedule.ts`), дата начала, вложение «Новый график» (необязательно). **Работник:** неполный рабочий день (часов) / неполная рабочая неделя (дней) — одно или оба, текст заявления. **Работодатель:** новый режим (вахтовый / сменный / иной + описание), новые график и время работы, причина (специфика объекта / смещение сроков проекта — только из ТЗ), обоснование. Поля другой роли при сохранении очищаются |
|
||||||
|
| Остальные 3 | — | ещё не разобраны с кадрами |
|
||||||
|
|
||||||
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
|
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
|
||||||
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { AllowanceSection } from './Allowance/AllowanceSection';
|
|||||||
import { PositionTransferSection } from './PositionTransfer/PositionTransferSection';
|
import { PositionTransferSection } from './PositionTransfer/PositionTransferSection';
|
||||||
import { DepartmentTransferSection } from './DepartmentTransfer/DepartmentTransferSection';
|
import { DepartmentTransferSection } from './DepartmentTransfer/DepartmentTransferSection';
|
||||||
import { WorkplaceChangeSection } from './WorkplaceChange/WorkplaceChangeSection';
|
import { WorkplaceChangeSection } from './WorkplaceChange/WorkplaceChangeSection';
|
||||||
|
import { ScheduleChangeSection } from './ScheduleChange/ScheduleChangeSection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
||||||
@@ -17,7 +18,8 @@ const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
|
|||||||
allowance: AllowanceSection,
|
allowance: AllowanceSection,
|
||||||
'position-transfer': PositionTransferSection,
|
'position-transfer': PositionTransferSection,
|
||||||
'department-transfer': DepartmentTransferSection,
|
'department-transfer': DepartmentTransferSection,
|
||||||
'workplace-change': WorkplaceChangeSection
|
'workplace-change': WorkplaceChangeSection,
|
||||||
|
'schedule-change': ScheduleChangeSection
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FormField, Select, TextInput, ISelectOption } from '../../../shared/ui';
|
||||||
|
import { OPTION_OTHER } from './workSchedule';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
/** Текст при «Другое». */
|
||||||
|
otherValue: string;
|
||||||
|
options: ISelectOption[];
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onOtherChange: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выбор из списка + ручной ввод при «Другое» (как график/время работы в подборе
|
||||||
|
* и приёме). Выбрали не «Другое» — ручной текст очищается, чтобы не сохранить
|
||||||
|
* скрытое значение. Поля кладутся прямо в сетку CollapseSection.
|
||||||
|
*/
|
||||||
|
export const OptionWithOtherField: React.FC<Props> = ({
|
||||||
|
label,
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
otherValue,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
onOtherChange
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<FormField label={label} htmlFor={id}>
|
||||||
|
<Select
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
onChange={(v) => {
|
||||||
|
onChange(v);
|
||||||
|
if (v !== OPTION_OTHER) onOtherChange('');
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{value === OPTION_OTHER && (
|
||||||
|
<FormField label={`${label} (другое)`} htmlFor={`${id}Other`}>
|
||||||
|
<TextInput id={`${id}Other`} value={otherValue} onChange={onOtherChange} />
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { CollapseSection, 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 { ISectionProps } from '../../model';
|
||||||
|
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
|
import { OptionWithOtherField } from '../OptionWithOtherField';
|
||||||
|
import { WORK_SCHEDULE_OPTIONS, WORK_HOURS_OPTIONS } from '../workSchedule';
|
||||||
|
import {
|
||||||
|
MODE_OTHER,
|
||||||
|
NEW_SCHEDULE_ATTACHMENT,
|
||||||
|
SCHEDULE_CHANGE_NEW_MODES,
|
||||||
|
SCHEDULE_CHANGE_REASONS
|
||||||
|
} from './options';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поля вида «Изменение режима / графика работы» — по ролям, строго по шаблонам ТЗ
|
||||||
|
* (см. options.ts). Общее: текущий режим (график + время работы), дата начала,
|
||||||
|
* необязательный документ «Новый график». Работник — неполное рабочее время
|
||||||
|
* (часов в день / дней в неделю) и текст заявления; работодатель — новый режим,
|
||||||
|
* новые график и время работы, причина и обоснование. Все поля необязательные.
|
||||||
|
*
|
||||||
|
* Поля — прямо в CollapseSection, без 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">
|
||||||
|
<OptionWithOtherField
|
||||||
|
label="Текущий график работы"
|
||||||
|
id="ScheduleChangeCurrentSchedule"
|
||||||
|
value={data.ScheduleChangeCurrentSchedule}
|
||||||
|
otherValue={data.ScheduleChangeCurrentScheduleOther}
|
||||||
|
options={WORK_SCHEDULE_OPTIONS}
|
||||||
|
onChange={(v) => set('ScheduleChangeCurrentSchedule', v)}
|
||||||
|
onOtherChange={(v) => set('ScheduleChangeCurrentScheduleOther', v)}
|
||||||
|
/>
|
||||||
|
<OptionWithOtherField
|
||||||
|
label="Текущее время работы"
|
||||||
|
id="ScheduleChangeCurrentHours"
|
||||||
|
value={data.ScheduleChangeCurrentHours}
|
||||||
|
otherValue={data.ScheduleChangeCurrentHoursOther}
|
||||||
|
options={WORK_HOURS_OPTIONS}
|
||||||
|
onChange={(v) => set('ScheduleChangeCurrentHours', v)}
|
||||||
|
onOtherChange={(v) => set('ScheduleChangeCurrentHoursOther', v)}
|
||||||
|
/>
|
||||||
|
<FormField label="Дата начала" htmlFor="ScheduleChangeDate" full>
|
||||||
|
<TextInput
|
||||||
|
id="ScheduleChangeDate"
|
||||||
|
type="date"
|
||||||
|
value={data.ScheduleChangeDate}
|
||||||
|
onChange={(v) => set('ScheduleChangeDate', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{isEmployee && (
|
||||||
|
<>
|
||||||
|
{/* Термины ТК: неполный рабочий день / неполная рабочая неделя — без них
|
||||||
|
«часов в день» читалось непонятно к чему. Заполнить можно одно или оба. */}
|
||||||
|
<FormField label="Неполный рабочий день, часов" htmlFor="ScheduleChangeHoursPerDay">
|
||||||
|
<TextInput
|
||||||
|
id="ScheduleChangeHoursPerDay"
|
||||||
|
type="number"
|
||||||
|
value={data.ScheduleChangeHoursPerDay}
|
||||||
|
onChange={(v) => set('ScheduleChangeHoursPerDay', v)}
|
||||||
|
placeholder="например, 6"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Неполная рабочая неделя, дней" htmlFor="ScheduleChangeDaysPerWeek">
|
||||||
|
<TextInput
|
||||||
|
id="ScheduleChangeDaysPerWeek"
|
||||||
|
type="number"
|
||||||
|
value={data.ScheduleChangeDaysPerWeek}
|
||||||
|
onChange={(v) => set('ScheduleChangeDaysPerWeek', v)}
|
||||||
|
placeholder="например, 4"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isEmployee && (
|
||||||
|
<>
|
||||||
|
<FormField label="Новый режим" htmlFor="ScheduleChangeNewMode" full>
|
||||||
|
<Select
|
||||||
|
id="ScheduleChangeNewMode"
|
||||||
|
value={data.ScheduleChangeNewMode}
|
||||||
|
onChange={(v) => {
|
||||||
|
set('ScheduleChangeNewMode', v);
|
||||||
|
if (v !== MODE_OTHER) set('ScheduleChangeNewModeOther', '');
|
||||||
|
}}
|
||||||
|
options={SCHEDULE_CHANGE_NEW_MODES}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{data.ScheduleChangeNewMode === MODE_OTHER && (
|
||||||
|
<FormField label="Иной режим (описание)" htmlFor="ScheduleChangeNewModeOther" full>
|
||||||
|
<TextInput
|
||||||
|
id="ScheduleChangeNewModeOther"
|
||||||
|
value={data.ScheduleChangeNewModeOther}
|
||||||
|
onChange={(v) => set('ScheduleChangeNewModeOther', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
<OptionWithOtherField
|
||||||
|
label="Новый график работы"
|
||||||
|
id="ScheduleChangeNewSchedule"
|
||||||
|
value={data.ScheduleChangeNewSchedule}
|
||||||
|
otherValue={data.ScheduleChangeNewScheduleOther}
|
||||||
|
options={WORK_SCHEDULE_OPTIONS}
|
||||||
|
onChange={(v) => set('ScheduleChangeNewSchedule', v)}
|
||||||
|
onOtherChange={(v) => set('ScheduleChangeNewScheduleOther', v)}
|
||||||
|
/>
|
||||||
|
<OptionWithOtherField
|
||||||
|
label="Новое время работы"
|
||||||
|
id="ScheduleChangeNewHours"
|
||||||
|
value={data.ScheduleChangeNewHours}
|
||||||
|
otherValue={data.ScheduleChangeNewHoursOther}
|
||||||
|
options={WORK_HOURS_OPTIONS}
|
||||||
|
onChange={(v) => set('ScheduleChangeNewHours', v)}
|
||||||
|
onOtherChange={(v) => set('ScheduleChangeNewHoursOther', v)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<InitiatorReasonFields
|
||||||
|
initiator={data.ChangeInitiator}
|
||||||
|
reason={{
|
||||||
|
id: 'ScheduleChangeReason',
|
||||||
|
value: data.ScheduleChangeReason,
|
||||||
|
options: SCHEDULE_CHANGE_REASONS,
|
||||||
|
onChange: (v) => set('ScheduleChangeReason', v)
|
||||||
|
}}
|
||||||
|
justification={{
|
||||||
|
id: 'ScheduleChangeJustification',
|
||||||
|
value: data.ScheduleChangeJustification,
|
||||||
|
onChange: (v) => set('ScheduleChangeJustification', v)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{attachments && (
|
||||||
|
<AttachmentSlotField
|
||||||
|
label={NEW_SCHEDULE_ATTACHMENT}
|
||||||
|
editor={attachments}
|
||||||
|
editable={!readOnly}
|
||||||
|
groupKey={NEW_SCHEDULE_ATTACHMENT}
|
||||||
|
category={NEW_SCHEDULE_ATTACHMENT}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CollapseSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ISelectOption } from '../../../../shared/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вид «Изменение режима / графика работы». По ТЗ шаблон зависит от инициатора:
|
||||||
|
* - работник: «прошу установить неполное рабочее время (___ ч в день / ___ дней
|
||||||
|
* в неделю) с __.__.20__ (текст)»;
|
||||||
|
* - работодатель: «прошу перевести работника на а) вахтовый метод; б) сменный
|
||||||
|
* график; в) иной режим (текст) с __.__.20__ в связи со спецификой объекта /
|
||||||
|
* смещением сроков проекта».
|
||||||
|
* Значения = choices колонок ScheduleChangeNewMode / ScheduleChangeReason.
|
||||||
|
*/
|
||||||
|
export const MODE_OTHER = 'Иной режим';
|
||||||
|
export const SCHEDULE_CHANGE_NEW_MODES: ISelectOption[] = ['Вахтовый метод', 'Сменный график', MODE_OTHER].map(
|
||||||
|
(mode) => ({ value: mode, label: mode })
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Причины работодателя — ровно те, что в шаблоне ТЗ («Иное» в ТЗ нет — не добавляем). */
|
||||||
|
export const SCHEDULE_CHANGE_REASONS: ISelectOption[] = ['Специфика объекта', 'Смещение сроков проекта'].map(
|
||||||
|
(reason) => ({ value: reason, label: reason })
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Вложение «Новый график» — по ТЗ «можно приложить документ», необязательно. */
|
||||||
|
export const NEW_SCHEDULE_ATTACHMENT = 'Новый график';
|
||||||
+8
-18
@@ -3,7 +3,9 @@ import { CollapseSection, FormField, TextInput, TextArea, Select, Toggle } from
|
|||||||
import { ProjectPicker } from '../../../../shared/dictionary';
|
import { ProjectPicker } from '../../../../shared/dictionary';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { WORKPLACE_CHANGE_REASONS, WORK_SCHEDULE_OPTIONS, SCHEDULE_OTHER } from './options';
|
import { WORKPLACE_CHANGE_REASONS } from './options';
|
||||||
|
import { WORK_SCHEDULE_OPTIONS } from '../workSchedule';
|
||||||
|
import { OptionWithOtherField } from '../OptionWithOtherField';
|
||||||
import { useCompanyOptions } from './useCompanyOptions';
|
import { useCompanyOptions } from './useCompanyOptions';
|
||||||
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
import styles from './WorkplaceChangeSection.module.scss';
|
import styles from './WorkplaceChangeSection.module.scss';
|
||||||
@@ -102,27 +104,15 @@ export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, con
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
{data.WorkplaceChangeRotation && (
|
{data.WorkplaceChangeRotation && (
|
||||||
<FormField label="График работы" htmlFor="WorkplaceChangeSchedule">
|
<OptionWithOtherField
|
||||||
<Select
|
label="График работы"
|
||||||
id="WorkplaceChangeSchedule"
|
id="WorkplaceChangeSchedule"
|
||||||
value={data.WorkplaceChangeSchedule}
|
value={data.WorkplaceChangeSchedule}
|
||||||
onChange={(v) => {
|
otherValue={data.WorkplaceChangeScheduleOther}
|
||||||
set('WorkplaceChangeSchedule', v);
|
|
||||||
if (v !== SCHEDULE_OTHER) set('WorkplaceChangeScheduleOther', '');
|
|
||||||
}}
|
|
||||||
options={WORK_SCHEDULE_OPTIONS}
|
options={WORK_SCHEDULE_OPTIONS}
|
||||||
placeholder="— выберите —"
|
onChange={(v) => set('WorkplaceChangeSchedule', v)}
|
||||||
|
onOtherChange={(v) => set('WorkplaceChangeScheduleOther', v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
|
||||||
)}
|
|
||||||
{data.WorkplaceChangeRotation && data.WorkplaceChangeSchedule === SCHEDULE_OTHER && (
|
|
||||||
<FormField label="График работы (другое)" htmlFor="WorkplaceChangeScheduleOther">
|
|
||||||
<TextInput
|
|
||||||
id="WorkplaceChangeScheduleOther"
|
|
||||||
value={data.WorkplaceChangeScheduleOther}
|
|
||||||
onChange={(v) => set('WorkplaceChangeScheduleOther', v)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
)}
|
)}
|
||||||
<InitiatorReasonFields
|
<InitiatorReasonFields
|
||||||
initiator={data.ChangeInitiator}
|
initiator={data.ChangeInitiator}
|
||||||
|
|||||||
@@ -10,24 +10,3 @@ export const WORKPLACE_CHANGE_REASONS: ISelectOption[] = [
|
|||||||
'Переезд подразделения / работодателя в другую местность',
|
'Переезд подразделения / работодателя в другую местность',
|
||||||
'Иное'
|
'Иное'
|
||||||
].map((reason) => ({ value: reason, label: reason }));
|
].map((reason) => ({ value: reason, label: reason }));
|
||||||
|
|
||||||
/** «Другое» в графике работы — открывает ручной ввод. */
|
|
||||||
export const SCHEDULE_OTHER = 'Другое';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Варианты «График работы» при вахте — тот же набор, что в «Заявке на подбор» и
|
|
||||||
* «Заявке на приём» (WORK_SCHEDULE_OPTIONS там): формы независимы, значения
|
|
||||||
* продублированы намеренно, как и между ними.
|
|
||||||
*/
|
|
||||||
export const WORK_SCHEDULE_OPTIONS: ISelectOption[] = [
|
|
||||||
'5/2',
|
|
||||||
'Вахтовый 30/30',
|
|
||||||
'Вахтовый 20/10',
|
|
||||||
'Вахтовый 30/15',
|
|
||||||
'Вахтовый 22/8',
|
|
||||||
'Вахтовый 15/15',
|
|
||||||
'6/1',
|
|
||||||
'1/2',
|
|
||||||
'2/2',
|
|
||||||
SCHEDULE_OTHER
|
|
||||||
].map((schedule) => ({ value: schedule, label: schedule }));
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { attachmentsOfCategory, prefixFiles } from '../../../shared/attachments/
|
|||||||
import { IFormData } from '../model';
|
import { IFormData } from '../model';
|
||||||
import { ATTESTATION_ATTACHMENT, requiresAttestation } from './Allowance/options';
|
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
|
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
|
||||||
@@ -23,6 +24,10 @@ export function activeAttachmentSlots(data: IFormData): IAttachmentSlot[] {
|
|||||||
if (data.ChangeTypes.includes('position-transfer')) {
|
if (data.ChangeTypes.includes('position-transfer')) {
|
||||||
slots.push({ category: JOB_DESCRIPTION_ATTACHMENT, required: true });
|
slots.push({ category: JOB_DESCRIPTION_ATTACHMENT, required: true });
|
||||||
}
|
}
|
||||||
|
if (data.ChangeTypes.includes('schedule-change')) {
|
||||||
|
// «Можно приложить документ» — необязательно.
|
||||||
|
slots.push({ category: NEW_SCHEDULE_ATTACHMENT, required: false });
|
||||||
|
}
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ISelectOption } from '../../../shared/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Режим работы: «График работы» и «Время работы» — те же наборы, что в «Заявке
|
||||||
|
* на подбор» и «Заявке на приём» (WORK_SCHEDULE_OPTIONS / WORK_HOURS_OPTIONS там):
|
||||||
|
* формы независимы, значения продублированы намеренно, как и между ними.
|
||||||
|
* Общие для видов этой формы (место работы — вахта, режим/график работы).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** «Другое» в списке — открывает ручной ввод (см. OptionWithOtherField). */
|
||||||
|
export const OPTION_OTHER = 'Другое';
|
||||||
|
|
||||||
|
function toOptions(values: string[]): ISelectOption[] {
|
||||||
|
return values.map((value) => ({ value, label: value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORK_SCHEDULE_OPTIONS: ISelectOption[] = toOptions([
|
||||||
|
'5/2',
|
||||||
|
'Вахтовый 30/30',
|
||||||
|
'Вахтовый 20/10',
|
||||||
|
'Вахтовый 30/15',
|
||||||
|
'Вахтовый 22/8',
|
||||||
|
'Вахтовый 15/15',
|
||||||
|
'6/1',
|
||||||
|
'1/2',
|
||||||
|
'2/2',
|
||||||
|
OPTION_OTHER
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 24-часовой формат, как в подборе и приёме. */
|
||||||
|
export const WORK_HOURS_OPTIONS: ISelectOption[] = toOptions([
|
||||||
|
'08:00-17:00',
|
||||||
|
'09:00-18:00',
|
||||||
|
'07:00-19:00',
|
||||||
|
'08:00-20:00',
|
||||||
|
'09:00-21:00',
|
||||||
|
OPTION_OTHER
|
||||||
|
]);
|
||||||
@@ -124,7 +124,22 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
'WorkplaceChangeRotation',
|
'WorkplaceChangeRotation',
|
||||||
'WorkplaceChangeSchedule',
|
'WorkplaceChangeSchedule',
|
||||||
'WorkplaceChangeScheduleOther',
|
'WorkplaceChangeScheduleOther',
|
||||||
'WorkplaceChangeJustification'
|
'WorkplaceChangeJustification',
|
||||||
|
'ScheduleChangeCurrentSchedule',
|
||||||
|
'ScheduleChangeCurrentScheduleOther',
|
||||||
|
'ScheduleChangeCurrentHours',
|
||||||
|
'ScheduleChangeCurrentHoursOther',
|
||||||
|
'ScheduleChangeDate',
|
||||||
|
'ScheduleChangeHoursPerDay',
|
||||||
|
'ScheduleChangeDaysPerWeek',
|
||||||
|
'ScheduleChangeNewMode',
|
||||||
|
'ScheduleChangeNewModeOther',
|
||||||
|
'ScheduleChangeNewSchedule',
|
||||||
|
'ScheduleChangeNewScheduleOther',
|
||||||
|
'ScheduleChangeNewHours',
|
||||||
|
'ScheduleChangeNewHoursOther',
|
||||||
|
'ScheduleChangeReason',
|
||||||
|
'ScheduleChangeJustification'
|
||||||
)
|
)
|
||||||
.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;
|
||||||
@@ -170,7 +185,22 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
WorkplaceChangeRotation: item.WorkplaceChangeRotation === true,
|
WorkplaceChangeRotation: item.WorkplaceChangeRotation === true,
|
||||||
WorkplaceChangeSchedule: text(item.WorkplaceChangeSchedule),
|
WorkplaceChangeSchedule: text(item.WorkplaceChangeSchedule),
|
||||||
WorkplaceChangeScheduleOther: text(item.WorkplaceChangeScheduleOther),
|
WorkplaceChangeScheduleOther: text(item.WorkplaceChangeScheduleOther),
|
||||||
WorkplaceChangeJustification: text(item.WorkplaceChangeJustification)
|
WorkplaceChangeJustification: text(item.WorkplaceChangeJustification),
|
||||||
|
ScheduleChangeCurrentSchedule: text(item.ScheduleChangeCurrentSchedule),
|
||||||
|
ScheduleChangeCurrentScheduleOther: text(item.ScheduleChangeCurrentScheduleOther),
|
||||||
|
ScheduleChangeCurrentHours: text(item.ScheduleChangeCurrentHours),
|
||||||
|
ScheduleChangeCurrentHoursOther: text(item.ScheduleChangeCurrentHoursOther),
|
||||||
|
ScheduleChangeDate: spDateOnlyToIso(text(item.ScheduleChangeDate)),
|
||||||
|
ScheduleChangeHoursPerDay: numberText(item.ScheduleChangeHoursPerDay),
|
||||||
|
ScheduleChangeDaysPerWeek: numberText(item.ScheduleChangeDaysPerWeek),
|
||||||
|
ScheduleChangeNewMode: text(item.ScheduleChangeNewMode),
|
||||||
|
ScheduleChangeNewModeOther: text(item.ScheduleChangeNewModeOther),
|
||||||
|
ScheduleChangeNewSchedule: text(item.ScheduleChangeNewSchedule),
|
||||||
|
ScheduleChangeNewScheduleOther: text(item.ScheduleChangeNewScheduleOther),
|
||||||
|
ScheduleChangeNewHours: text(item.ScheduleChangeNewHours),
|
||||||
|
ScheduleChangeNewHoursOther: text(item.ScheduleChangeNewHoursOther),
|
||||||
|
ScheduleChangeReason: text(item.ScheduleChangeReason),
|
||||||
|
ScheduleChangeJustification: text(item.ScheduleChangeJustification)
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -126,6 +126,31 @@ export interface IFormData {
|
|||||||
WorkplaceChangeScheduleOther: string;
|
WorkplaceChangeScheduleOther: string;
|
||||||
/** Обоснование; при «Иное» — его текст, у работника — текст заявления. */
|
/** Обоснование; при «Иное» — его текст, у работника — текст заявления. */
|
||||||
WorkplaceChangeJustification: string;
|
WorkplaceChangeJustification: string;
|
||||||
|
|
||||||
|
// ── Вид «Изменение режима / графика работы» (byType/ScheduleChange). Поля по ролям
|
||||||
|
// строго по ТЗ (см. ScheduleChange/options.ts). Все необязательные; «Другое» → *Other.
|
||||||
|
/** Общее: текущий график и время работы (WORK_SCHEDULE_OPTIONS / WORK_HOURS_OPTIONS). */
|
||||||
|
ScheduleChangeCurrentSchedule: string;
|
||||||
|
ScheduleChangeCurrentScheduleOther: string;
|
||||||
|
ScheduleChangeCurrentHours: string;
|
||||||
|
ScheduleChangeCurrentHoursOther: string;
|
||||||
|
/** Общее: с какой даты, ISO YYYY-MM-DD. */
|
||||||
|
ScheduleChangeDate: string;
|
||||||
|
/** Работник: неполное рабочее время — часов в день и дней в неделю (строка из input type=number). */
|
||||||
|
ScheduleChangeHoursPerDay: string;
|
||||||
|
ScheduleChangeDaysPerWeek: string;
|
||||||
|
/** Работодатель: новый режим (SCHEDULE_CHANGE_NEW_MODES); при «Иной режим» — описание. */
|
||||||
|
ScheduleChangeNewMode: string;
|
||||||
|
ScheduleChangeNewModeOther: string;
|
||||||
|
/** Работодатель: новые график и время работы. */
|
||||||
|
ScheduleChangeNewSchedule: string;
|
||||||
|
ScheduleChangeNewScheduleOther: string;
|
||||||
|
ScheduleChangeNewHours: string;
|
||||||
|
ScheduleChangeNewHoursOther: string;
|
||||||
|
/** Работодатель: причина (SCHEDULE_CHANGE_REASONS). */
|
||||||
|
ScheduleChangeReason: string;
|
||||||
|
/** Работодатель — обоснование, работник — текст заявления. */
|
||||||
|
ScheduleChangeJustification: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновление одного поля формы с сохранением типа значения. */
|
/** Обновление одного поля формы с сохранением типа значения. */
|
||||||
@@ -179,7 +204,22 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
WorkplaceChangeRotation: false,
|
WorkplaceChangeRotation: false,
|
||||||
WorkplaceChangeSchedule: '',
|
WorkplaceChangeSchedule: '',
|
||||||
WorkplaceChangeScheduleOther: '',
|
WorkplaceChangeScheduleOther: '',
|
||||||
WorkplaceChangeJustification: ''
|
WorkplaceChangeJustification: '',
|
||||||
|
ScheduleChangeCurrentSchedule: '',
|
||||||
|
ScheduleChangeCurrentScheduleOther: '',
|
||||||
|
ScheduleChangeCurrentHours: '',
|
||||||
|
ScheduleChangeCurrentHoursOther: '',
|
||||||
|
ScheduleChangeDate: '',
|
||||||
|
ScheduleChangeHoursPerDay: '',
|
||||||
|
ScheduleChangeDaysPerWeek: '',
|
||||||
|
ScheduleChangeNewMode: '',
|
||||||
|
ScheduleChangeNewModeOther: '',
|
||||||
|
ScheduleChangeNewSchedule: '',
|
||||||
|
ScheduleChangeNewScheduleOther: '',
|
||||||
|
ScheduleChangeNewHours: '',
|
||||||
|
ScheduleChangeNewHoursOther: '',
|
||||||
|
ScheduleChangeReason: '',
|
||||||
|
ScheduleChangeJustification: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Плейсхолдер для поля выбора сотрудника. */
|
/** Плейсхолдер для поля выбора сотрудника. */
|
||||||
|
|||||||
@@ -90,6 +90,22 @@
|
|||||||
{ "internalName": "WorkplaceChangeRotation", "title": "Место работы: вахта", "type": "Boolean" },
|
{ "internalName": "WorkplaceChangeRotation", "title": "Место работы: вахта", "type": "Boolean" },
|
||||||
{ "internalName": "WorkplaceChangeSchedule", "title": "Место работы: график работы", "type": "Text" },
|
{ "internalName": "WorkplaceChangeSchedule", "title": "Место работы: график работы", "type": "Text" },
|
||||||
{ "internalName": "WorkplaceChangeScheduleOther", "title": "Место работы: график работы (другое)", "type": "Text" },
|
{ "internalName": "WorkplaceChangeScheduleOther", "title": "Место работы: график работы (другое)", "type": "Text" },
|
||||||
{ "internalName": "WorkplaceChangeJustification", "title": "Место работы: обоснование", "type": "Note" }
|
{ "internalName": "WorkplaceChangeJustification", "title": "Место работы: обоснование", "type": "Note" },
|
||||||
|
|
||||||
|
{ "internalName": "ScheduleChangeCurrentSchedule", "title": "Режим работы: текущий график", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeCurrentScheduleOther", "title": "Режим работы: текущий график (другое)", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeCurrentHours", "title": "Режим работы: текущее время работы", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeCurrentHoursOther", "title": "Режим работы: текущее время работы (другое)", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeDate", "title": "Режим работы: дата начала", "type": "DateTime", "dateOnly": true },
|
||||||
|
{ "internalName": "ScheduleChangeHoursPerDay", "title": "Режим работы: неполный рабочий день, часов", "type": "Number", "decimals": 1 },
|
||||||
|
{ "internalName": "ScheduleChangeDaysPerWeek", "title": "Режим работы: неполная рабочая неделя, дней", "type": "Number", "decimals": 0 },
|
||||||
|
{ "internalName": "ScheduleChangeNewMode", "title": "Режим работы: новый режим", "type": "Choice", "choices": ["Вахтовый метод", "Сменный график", "Иной режим"] },
|
||||||
|
{ "internalName": "ScheduleChangeNewModeOther", "title": "Режим работы: иной режим (описание)", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeNewSchedule", "title": "Режим работы: новый график", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeNewScheduleOther", "title": "Режим работы: новый график (другое)", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeNewHours", "title": "Режим работы: новое время работы", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeNewHoursOther", "title": "Режим работы: новое время работы (другое)", "type": "Text" },
|
||||||
|
{ "internalName": "ScheduleChangeReason", "title": "Режим работы: причина", "type": "Choice", "choices": ["Специфика объекта", "Смещение сроков проекта"] },
|
||||||
|
{ "internalName": "ScheduleChangeJustification", "title": "Режим работы: обоснование / текст заявления", "type": "Note" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,28 +149,79 @@ function departmentTransferColumns(data: IFormData, receivingHeadId: number | un
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Колонки вида «Изменение места работы»; вид не выбран — очищаются. */
|
/** Колонки вида «Изменение места работы»; вид не выбран — очищаются, график — только при вахте. */
|
||||||
function workplaceChangeColumns(data: IFormData): Record<string, unknown> {
|
function workplaceChangeColumns(data: IFormData): Record<string, unknown> {
|
||||||
const columns = {
|
const isSelected = data.ChangeTypes.includes('workplace-change');
|
||||||
WorkplaceChangeCurrentProject: data.WorkplaceChangeCurrentProject.trim() || null,
|
const fields = keepOrClear(isSelected, {
|
||||||
WorkplaceChangeCurrentProjectId: data.WorkplaceChangeCurrentProjectId || null,
|
WorkplaceChangeCurrentProject: data.WorkplaceChangeCurrentProject.trim(),
|
||||||
WorkplaceChangeNewCompany: data.WorkplaceChangeNewCompany || null,
|
WorkplaceChangeCurrentProjectId: data.WorkplaceChangeCurrentProjectId,
|
||||||
WorkplaceChangeNewCompanyId: data.WorkplaceChangeNewCompanyId || null,
|
WorkplaceChangeNewCompany: data.WorkplaceChangeNewCompany,
|
||||||
WorkplaceChangeNewProject: data.WorkplaceChangeNewProject.trim() || null,
|
WorkplaceChangeNewCompanyId: data.WorkplaceChangeNewCompanyId,
|
||||||
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId || null,
|
WorkplaceChangeNewProject: data.WorkplaceChangeNewProject.trim(),
|
||||||
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim() || null,
|
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId,
|
||||||
WorkplaceChangeDate: data.WorkplaceChangeDate || null,
|
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim(),
|
||||||
WorkplaceChangeReason: employerReason(data, data.WorkplaceChangeReason) ?? null,
|
WorkplaceChangeDate: data.WorkplaceChangeDate,
|
||||||
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation || null,
|
WorkplaceChangeReason: employerReason(data, data.WorkplaceChangeReason),
|
||||||
|
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation,
|
||||||
WorkplaceChangeRotation: data.WorkplaceChangeRotation,
|
WorkplaceChangeRotation: data.WorkplaceChangeRotation,
|
||||||
|
WorkplaceChangeJustification: data.WorkplaceChangeJustification
|
||||||
|
});
|
||||||
// Без вахты графика нет, даже если он остался в состоянии формы.
|
// Без вахты графика нет, даже если он остался в состоянии формы.
|
||||||
WorkplaceChangeSchedule: (data.WorkplaceChangeRotation && data.WorkplaceChangeSchedule) || null,
|
const rotationFields = keepOrClear(isSelected && data.WorkplaceChangeRotation, {
|
||||||
WorkplaceChangeScheduleOther: (data.WorkplaceChangeRotation && data.WorkplaceChangeScheduleOther.trim()) || null,
|
WorkplaceChangeSchedule: data.WorkplaceChangeSchedule,
|
||||||
WorkplaceChangeJustification: data.WorkplaceChangeJustification || null
|
WorkplaceChangeScheduleOther: data.WorkplaceChangeScheduleOther.trim()
|
||||||
};
|
});
|
||||||
if (data.ChangeTypes.includes('workplace-change')) return columns;
|
return { ...fields, ...rotationFields };
|
||||||
// Тот же набор ключей, все — null: колонки невыбранного вида очищаются.
|
}
|
||||||
return Object.keys(columns).reduce<Record<string, unknown>>((acc, key) => ({ ...acc, [key]: null }), {});
|
|
||||||
|
/**
|
||||||
|
* Колонки вида «Изменение режима / графика работы». Поля другой роли очищаются:
|
||||||
|
* у работника нет нового режима и причин, у работодателя — часов/дней неполного времени.
|
||||||
|
*/
|
||||||
|
function scheduleChangeColumns(data: IFormData): Record<string, unknown> {
|
||||||
|
const isSelected = data.ChangeTypes.includes('schedule-change');
|
||||||
|
const isEmployee = data.ChangeInitiator === INITIATOR_EMPLOYEE;
|
||||||
|
|
||||||
|
const common = keepOrClear(isSelected, {
|
||||||
|
ScheduleChangeCurrentSchedule: data.ScheduleChangeCurrentSchedule,
|
||||||
|
ScheduleChangeCurrentScheduleOther: data.ScheduleChangeCurrentScheduleOther.trim(),
|
||||||
|
ScheduleChangeCurrentHours: data.ScheduleChangeCurrentHours,
|
||||||
|
ScheduleChangeCurrentHoursOther: data.ScheduleChangeCurrentHoursOther.trim(),
|
||||||
|
ScheduleChangeDate: data.ScheduleChangeDate,
|
||||||
|
ScheduleChangeJustification: data.ScheduleChangeJustification
|
||||||
|
});
|
||||||
|
const employeeFields = keepOrClear(isSelected && isEmployee, {
|
||||||
|
ScheduleChangeHoursPerDay: numberColumn(data.ScheduleChangeHoursPerDay),
|
||||||
|
ScheduleChangeDaysPerWeek: numberColumn(data.ScheduleChangeDaysPerWeek)
|
||||||
|
});
|
||||||
|
const employerFields = keepOrClear(isSelected && !isEmployee, {
|
||||||
|
ScheduleChangeNewMode: data.ScheduleChangeNewMode,
|
||||||
|
ScheduleChangeNewModeOther: data.ScheduleChangeNewModeOther.trim(),
|
||||||
|
ScheduleChangeNewSchedule: data.ScheduleChangeNewSchedule,
|
||||||
|
ScheduleChangeNewScheduleOther: data.ScheduleChangeNewScheduleOther.trim(),
|
||||||
|
ScheduleChangeNewHours: data.ScheduleChangeNewHours,
|
||||||
|
ScheduleChangeNewHoursOther: data.ScheduleChangeNewHoursOther.trim(),
|
||||||
|
ScheduleChangeReason: data.ScheduleChangeReason
|
||||||
|
});
|
||||||
|
return { ...common, ...employeeFields, ...employerFields };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Группа колонок: keep — значения как есть (пустые '' / undefined → null; false
|
||||||
|
* остаётся false), иначе все → null, чтобы SharePoint очистил поля, которых при
|
||||||
|
* текущем виде/роли на форме нет.
|
||||||
|
*/
|
||||||
|
function keepOrClear(
|
||||||
|
keep: boolean,
|
||||||
|
columns: Record<string, string | number | boolean | undefined>
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
Object.keys(columns).forEach((key) => {
|
||||||
|
const value = columns[key];
|
||||||
|
const isEmpty = value === undefined || value === '';
|
||||||
|
result[key] = keep && !isEmpty ? value : null;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Поля элемента списка данных из модели формы. */
|
/** Поля элемента списка данных из модели формы. */
|
||||||
@@ -186,7 +237,8 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
|
|||||||
...allowanceColumns(data),
|
...allowanceColumns(data),
|
||||||
...positionTransferColumns(data),
|
...positionTransferColumns(data),
|
||||||
...departmentTransferColumns(data, receivingHeadId),
|
...departmentTransferColumns(data, receivingHeadId),
|
||||||
...workplaceChangeColumns(data)
|
...workplaceChangeColumns(data),
|
||||||
|
...scheduleChangeColumns(data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user