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

Восьмой вид, ровно по шаблону ТЗ: дата изменения, «в связи с» свободным
текстом (списка причин в ТЗ нет) и обязательная новая должностная
инструкция — отдельный слот вложений. Инициатор — только руководитель.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-29 15:24:55 +05:00
parent 55659f94cf
commit f61418dbe2
9 changed files with 89 additions and 7 deletions
@@ -64,7 +64,8 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование | | Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
| Продление срока ТД | — | **пропущен** (2026-10-29): по ТЗ это автоматический запрос ОК за месяц до окончания срочного ТД (маршрут В), а не обычная заявка — решать отдельно | | Продление срока ТД | — | **пропущен** (2026-10-29): по ТЗ это автоматический запрос ОК за месяц до окончания срочного ТД (маршрут В), а не обычная заявка — решать отдельно |
| Изменение режима / графика работы | `ScheduleChange` | по ролям строго по ТЗ. Общее: текущий график + текущее время работы (списки как в подборе/приёме, «Другое» → текст; `OptionWithOtherField`, наборы — `byType/workSchedule.ts`), дата начала, вложение «Новый график» (необязательно). **Работник:** неполный рабочий день (часов) / неполная рабочая неделя (дней) — одно или оба, текст заявления. **Работодатель:** новый режим (вахтовый / сменный / иной + описание), новые график и время работы, причина (специфика объекта / смещение сроков проекта — только из ТЗ), обоснование. Поля другой роли при сохранении очищаются | | Изменение режима / графика работы | `ScheduleChange` | по ролям строго по ТЗ. Общее: текущий график + текущее время работы (списки как в подборе/приёме, «Другое» → текст; `OptionWithOtherField`, наборы — `byType/workSchedule.ts`), дата начала, вложение «Новый график» (необязательно). **Работник:** неполный рабочий день (часов) / неполная рабочая неделя (дней) — одно или оба, текст заявления. **Работодатель:** новый режим (вахтовый / сменный / иной + описание), новые график и время работы, причина (специфика объекта / смещение сроков проекта — только из ТЗ), обоснование. Поля другой роли при сохранении очищаются |
| Остальные 3 | — | ещё не разобраны с кадрами | | Изменение трудовой функции (без смены должности) | `FunctionChange` | ровно по шаблону ТЗ: дата изменения, «В связи с» (свободный текст — списка причин в ТЗ нет), вложение **«Новая должностная инструкция»** — обязательно. Инициатор — только руководитель |
| Остальные 2 | — | ещё не разобраны с кадрами |
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель, Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них. новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
@@ -7,6 +7,7 @@ import { PositionTransferSection } from './PositionTransfer/PositionTransferSect
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'; import { ScheduleChangeSection } from './ScheduleChange/ScheduleChangeSection';
import { FunctionChangeSection } from './FunctionChange/FunctionChangeSection';
/** /**
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере * Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
@@ -19,7 +20,8 @@ const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
'position-transfer': PositionTransferSection, 'position-transfer': PositionTransferSection,
'department-transfer': DepartmentTransferSection, 'department-transfer': DepartmentTransferSection,
'workplace-change': WorkplaceChangeSection, 'workplace-change': WorkplaceChangeSection,
'schedule-change': ScheduleChangeSection 'schedule-change': ScheduleChangeSection,
'function-change': FunctionChangeSection
}; };
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */ /** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
@@ -0,0 +1,50 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea } from '../../../../shared/ui';
import { useFormReadOnly } from '@tnsu/ui-kit';
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './options';
/**
* Поля вида «Изменение трудовой функции (без смены должности)». Ровно по шаблону
* ТЗ: «прошу внести изменения в трудовую функцию работника согласно ДИ во
* вложении с __.__.20__ в связи с ___ (текст)» — дата, «в связи с» (свободный
* текст, списка причин в ТЗ нет) и обязательная новая ДИ. Инициатор — только
* руководитель.
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
*/
export const FunctionChangeSection: React.FC<ISectionProps> = ({ data, set, attachments }) => {
const readOnly = useFormReadOnly();
return (
<CollapseSection title={CHANGE_TYPE_LABELS['function-change']} icon="document">
<FormField label="Дата изменения" htmlFor="FunctionChangeDate">
<TextInput
id="FunctionChangeDate"
type="date"
value={data.FunctionChangeDate}
onChange={(v) => set('FunctionChangeDate', v)}
/>
</FormField>
<FormField label="В связи с" htmlFor="FunctionChangeReason" full>
<TextArea
id="FunctionChangeReason"
value={data.FunctionChangeReason}
onChange={(v) => set('FunctionChangeReason', v)}
/>
</FormField>
{attachments && (
<AttachmentSlotField
label={NEW_JOB_DESCRIPTION_ATTACHMENT}
editor={attachments}
editable={!readOnly}
groupKey={NEW_JOB_DESCRIPTION_ATTACHMENT}
category={NEW_JOB_DESCRIPTION_ATTACHMENT}
required
/>
)}
</CollapseSection>
);
};
@@ -0,0 +1,5 @@
/**
* Вложение «Новая должностная инструкция» — по ТЗ «приложить документ», обязательно.
* Отдельная подпись от «Должностной инструкции» перевода на должность: это разные слоты.
*/
export const NEW_JOB_DESCRIPTION_ATTACHMENT = 'Новая должностная инструкция';
@@ -4,6 +4,7 @@ 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'; import { NEW_SCHEDULE_ATTACHMENT } from './ScheduleChange/options';
import { NEW_JOB_DESCRIPTION_ATTACHMENT } from './FunctionChange/options';
/** /**
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category) * Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
@@ -24,6 +25,9 @@ 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('function-change')) {
slots.push({ category: NEW_JOB_DESCRIPTION_ATTACHMENT, required: true });
}
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 });
@@ -139,7 +139,9 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
'ScheduleChangeNewHours', 'ScheduleChangeNewHours',
'ScheduleChangeNewHoursOther', 'ScheduleChangeNewHoursOther',
'ScheduleChangeReason', 'ScheduleChangeReason',
'ScheduleChangeJustification' 'ScheduleChangeJustification',
'FunctionChangeDate',
'FunctionChangeReason'
) )
.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;
@@ -200,7 +202,9 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
ScheduleChangeNewHours: text(item.ScheduleChangeNewHours), ScheduleChangeNewHours: text(item.ScheduleChangeNewHours),
ScheduleChangeNewHoursOther: text(item.ScheduleChangeNewHoursOther), ScheduleChangeNewHoursOther: text(item.ScheduleChangeNewHoursOther),
ScheduleChangeReason: text(item.ScheduleChangeReason), ScheduleChangeReason: text(item.ScheduleChangeReason),
ScheduleChangeJustification: text(item.ScheduleChangeJustification) ScheduleChangeJustification: text(item.ScheduleChangeJustification),
FunctionChangeDate: spDateOnlyToIso(text(item.FunctionChangeDate)),
FunctionChangeReason: text(item.FunctionChangeReason)
}; };
return { return {
@@ -151,6 +151,13 @@ export interface IFormData {
ScheduleChangeReason: string; ScheduleChangeReason: string;
/** Работодатель — обоснование, работник — текст заявления. */ /** Работодатель — обоснование, работник — текст заявления. */
ScheduleChangeJustification: string; ScheduleChangeJustification: string;
// ── Вид «Изменение трудовой функции» (byType/FunctionChange). Только работодатель.
// Поля необязательные; вложение «Новая должностная инструкция» — обязательно (attachmentSlots).
/** С какой даты, ISO YYYY-MM-DD. */
FunctionChangeDate: string;
/** «В связи с ___ (текст)» из шаблона ТЗ — списка причин нет. */
FunctionChangeReason: string;
} }
/** Обновление одного поля формы с сохранением типа значения. */ /** Обновление одного поля формы с сохранением типа значения. */
@@ -219,7 +226,9 @@ export const EMPTY_DATA: IFormData = {
ScheduleChangeNewHours: '', ScheduleChangeNewHours: '',
ScheduleChangeNewHoursOther: '', ScheduleChangeNewHoursOther: '',
ScheduleChangeReason: '', ScheduleChangeReason: '',
ScheduleChangeJustification: '' ScheduleChangeJustification: '',
FunctionChangeDate: '',
FunctionChangeReason: ''
}; };
/** Плейсхолдер для поля выбора сотрудника. */ /** Плейсхолдер для поля выбора сотрудника. */
@@ -106,6 +106,9 @@
{ "internalName": "ScheduleChangeNewHours", "title": "Режим работы: новое время работы", "type": "Text" }, { "internalName": "ScheduleChangeNewHours", "title": "Режим работы: новое время работы", "type": "Text" },
{ "internalName": "ScheduleChangeNewHoursOther", "title": "Режим работы: новое время работы (другое)", "type": "Text" }, { "internalName": "ScheduleChangeNewHoursOther", "title": "Режим работы: новое время работы (другое)", "type": "Text" },
{ "internalName": "ScheduleChangeReason", "title": "Режим работы: причина", "type": "Choice", "choices": ["Специфика объекта", "Смещение сроков проекта"] }, { "internalName": "ScheduleChangeReason", "title": "Режим работы: причина", "type": "Choice", "choices": ["Специфика объекта", "Смещение сроков проекта"] },
{ "internalName": "ScheduleChangeJustification", "title": "Режим работы: обоснование / текст заявления", "type": "Note" } { "internalName": "ScheduleChangeJustification", "title": "Режим работы: обоснование / текст заявления", "type": "Note" },
{ "internalName": "FunctionChangeDate", "title": "Трудовая функция: дата изменения", "type": "DateTime", "dateOnly": true },
{ "internalName": "FunctionChangeReason", "title": "Трудовая функция: в связи с", "type": "Note" }
] ]
} }
@@ -238,7 +238,11 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
...positionTransferColumns(data), ...positionTransferColumns(data),
...departmentTransferColumns(data, receivingHeadId), ...departmentTransferColumns(data, receivingHeadId),
...workplaceChangeColumns(data), ...workplaceChangeColumns(data),
...scheduleChangeColumns(data) ...scheduleChangeColumns(data),
...keepOrClear(data.ChangeTypes.includes('function-change'), {
FunctionChangeDate: data.FunctionChangeDate,
FunctionChangeReason: data.FunctionChangeReason
})
}; };
} }