feat: поля вида «Изменение режима / графика работы» в изменении условий ТД

Седьмой вид, по ролям строго по шаблонам ТЗ. Работник просит неполный
рабочий день / неполную рабочую неделю и пишет заявление; работодатель
переводит на вахтовый, сменный или иной режим с новым графиком и
временем работы и указывает причину из ТЗ. Текущий режим и дата — у
обоих, документ «Новый график» необязателен. Списки графика и времени
работы и поле «список + Другое» стали общими для видов формы.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-29 15:18:28 +05:00
parent 040ab475b5
commit 55659f94cf
13 changed files with 449 additions and 70 deletions
@@ -62,7 +62,9 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
| Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно |
| Перевод в другое подразделение | `DepartmentTransfer` | новое подразделение (`DepartmentPicker`, имя + id), дата перевода, причина (усиление подразделения / перераспределение функций / вакансия / иное), обоснование, **принимающий руководитель** (People, обязателен при отправке) — см. ниже |
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
| Остальные 5 | — | ещё не разобраны с кадрами |
| Продление срока ТД | — | **пропущен** (2026-10-29): по ТЗ это автоматический запрос ОК за месяц до окончания срочного ТД (маршрут В), а не обычная заявка — решать отдельно |
| Изменение режима / графика работы | `ScheduleChange` | по ролям строго по ТЗ. Общее: текущий график + текущее время работы (списки как в подборе/приёме, «Другое» → текст; `OptionWithOtherField`, наборы — `byType/workSchedule.ts`), дата начала, вложение «Новый график» (необязательно). **Работник:** неполный рабочий день (часов) / неполная рабочая неделя (дней) — одно или оба, текст заявления. **Работодатель:** новый режим (вахтовый / сменный / иной + описание), новые график и время работы, причина (специфика объекта / смещение сроков проекта — только из ТЗ), обоснование. Поля другой роли при сохранении очищаются |
| Остальные 3 | — | ещё не разобраны с кадрами |
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
@@ -6,6 +6,7 @@ import { AllowanceSection } from './Allowance/AllowanceSection';
import { PositionTransferSection } from './PositionTransfer/PositionTransferSection';
import { DepartmentTransferSection } from './DepartmentTransfer/DepartmentTransferSection';
import { WorkplaceChangeSection } from './WorkplaceChange/WorkplaceChangeSection';
import { ScheduleChangeSection } from './ScheduleChange/ScheduleChangeSection';
/**
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
@@ -17,7 +18,8 @@ const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
allowance: AllowanceSection,
'position-transfer': PositionTransferSection,
'department-transfer': DepartmentTransferSection,
'workplace-change': WorkplaceChangeSection
'workplace-change': WorkplaceChangeSection,
'schedule-change': ScheduleChangeSection
};
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. 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>
)}
</>
);
@@ -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 = 'Новый график';
@@ -3,7 +3,9 @@ import { CollapseSection, FormField, TextInput, TextArea, Select, Toggle } from
import { ProjectPicker } from '../../../../shared/dictionary';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
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 { InitiatorReasonFields } from '../InitiatorReasonFields';
import styles from './WorkplaceChangeSection.module.scss';
@@ -102,27 +104,15 @@ export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, con
/>
</FormField>
{data.WorkplaceChangeRotation && (
<FormField label="График работы" htmlFor="WorkplaceChangeSchedule">
<Select
id="WorkplaceChangeSchedule"
value={data.WorkplaceChangeSchedule}
onChange={(v) => {
set('WorkplaceChangeSchedule', v);
if (v !== SCHEDULE_OTHER) set('WorkplaceChangeScheduleOther', '');
}}
options={WORK_SCHEDULE_OPTIONS}
placeholder="— выберите —"
/>
</FormField>
)}
{data.WorkplaceChangeRotation && data.WorkplaceChangeSchedule === SCHEDULE_OTHER && (
<FormField label="График работы (другое)" htmlFor="WorkplaceChangeScheduleOther">
<TextInput
id="WorkplaceChangeScheduleOther"
value={data.WorkplaceChangeScheduleOther}
onChange={(v) => set('WorkplaceChangeScheduleOther', v)}
/>
</FormField>
<OptionWithOtherField
label="График работы"
id="WorkplaceChangeSchedule"
value={data.WorkplaceChangeSchedule}
otherValue={data.WorkplaceChangeScheduleOther}
options={WORK_SCHEDULE_OPTIONS}
onChange={(v) => set('WorkplaceChangeSchedule', v)}
onOtherChange={(v) => set('WorkplaceChangeScheduleOther', v)}
/>
)}
<InitiatorReasonFields
initiator={data.ChangeInitiator}
@@ -10,24 +10,3 @@ export const WORKPLACE_CHANGE_REASONS: ISelectOption[] = [
'Переезд подразделения / работодателя в другую местность',
'Иное'
].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 { ATTESTATION_ATTACHMENT, requiresAttestation } from './Allowance/options';
import { JOB_DESCRIPTION_ATTACHMENT } from './PositionTransfer/options';
import { NEW_SCHEDULE_ATTACHMENT } from './ScheduleChange/options';
/**
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
@@ -23,6 +24,10 @@ export function activeAttachmentSlots(data: IFormData): IAttachmentSlot[] {
if (data.ChangeTypes.includes('position-transfer')) {
slots.push({ category: JOB_DESCRIPTION_ATTACHMENT, required: true });
}
if (data.ChangeTypes.includes('schedule-change')) {
// «Можно приложить документ» — необязательно.
slots.push({ category: NEW_SCHEDULE_ATTACHMENT, required: false });
}
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',
'WorkplaceChangeSchedule',
'WorkplaceChangeScheduleOther',
'WorkplaceChangeJustification'
'WorkplaceChangeJustification',
'ScheduleChangeCurrentSchedule',
'ScheduleChangeCurrentScheduleOther',
'ScheduleChangeCurrentHours',
'ScheduleChangeCurrentHoursOther',
'ScheduleChangeDate',
'ScheduleChangeHoursPerDay',
'ScheduleChangeDaysPerWeek',
'ScheduleChangeNewMode',
'ScheduleChangeNewModeOther',
'ScheduleChangeNewSchedule',
'ScheduleChangeNewScheduleOther',
'ScheduleChangeNewHours',
'ScheduleChangeNewHoursOther',
'ScheduleChangeReason',
'ScheduleChangeJustification'
)
.expand('DepartmentTransferReceivingHead')()) as Record<string, unknown>;
const receivingHead = item.DepartmentTransferReceivingHead as { EMail?: string } | undefined;
@@ -170,7 +185,22 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
WorkplaceChangeRotation: item.WorkplaceChangeRotation === true,
WorkplaceChangeSchedule: text(item.WorkplaceChangeSchedule),
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 {
@@ -126,6 +126,31 @@ export interface IFormData {
WorkplaceChangeScheduleOther: 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,
WorkplaceChangeSchedule: '',
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": "WorkplaceChangeSchedule", "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> {
const columns = {
WorkplaceChangeCurrentProject: data.WorkplaceChangeCurrentProject.trim() || null,
WorkplaceChangeCurrentProjectId: data.WorkplaceChangeCurrentProjectId || null,
WorkplaceChangeNewCompany: data.WorkplaceChangeNewCompany || null,
WorkplaceChangeNewCompanyId: data.WorkplaceChangeNewCompanyId || null,
WorkplaceChangeNewProject: data.WorkplaceChangeNewProject.trim() || null,
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId || null,
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim() || null,
WorkplaceChangeDate: data.WorkplaceChangeDate || null,
WorkplaceChangeReason: employerReason(data, data.WorkplaceChangeReason) ?? null,
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation || null,
const isSelected = data.ChangeTypes.includes('workplace-change');
const fields = keepOrClear(isSelected, {
WorkplaceChangeCurrentProject: data.WorkplaceChangeCurrentProject.trim(),
WorkplaceChangeCurrentProjectId: data.WorkplaceChangeCurrentProjectId,
WorkplaceChangeNewCompany: data.WorkplaceChangeNewCompany,
WorkplaceChangeNewCompanyId: data.WorkplaceChangeNewCompanyId,
WorkplaceChangeNewProject: data.WorkplaceChangeNewProject.trim(),
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId,
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim(),
WorkplaceChangeDate: data.WorkplaceChangeDate,
WorkplaceChangeReason: employerReason(data, data.WorkplaceChangeReason),
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation,
WorkplaceChangeRotation: data.WorkplaceChangeRotation,
// Без вахты графика нет, даже если он остался в состоянии формы.
WorkplaceChangeSchedule: (data.WorkplaceChangeRotation && data.WorkplaceChangeSchedule) || null,
WorkplaceChangeScheduleOther: (data.WorkplaceChangeRotation && data.WorkplaceChangeScheduleOther.trim()) || null,
WorkplaceChangeJustification: data.WorkplaceChangeJustification || null
};
if (data.ChangeTypes.includes('workplace-change')) return columns;
// Тот же набор ключей, все — null: колонки невыбранного вида очищаются.
return Object.keys(columns).reduce<Record<string, unknown>>((acc, key) => ({ ...acc, [key]: null }), {});
WorkplaceChangeJustification: data.WorkplaceChangeJustification
});
// Без вахты графика нет, даже если он остался в состоянии формы.
const rotationFields = keepOrClear(isSelected && data.WorkplaceChangeRotation, {
WorkplaceChangeSchedule: data.WorkplaceChangeSchedule,
WorkplaceChangeScheduleOther: data.WorkplaceChangeScheduleOther.trim()
});
return { ...fields, ...rotationFields };
}
/**
* Колонки вида «Изменение режима / графика работы». Поля другой роли очищаются:
* у работника нет нового режима и причин, у работодателя — часов/дней неполного времени.
*/
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),
...positionTransferColumns(data),
...departmentTransferColumns(data, receivingHeadId),
...workplaceChangeColumns(data)
...workplaceChangeColumns(data),
...scheduleChangeColumns(data)
};
}