feat: поля вида «Перевод на другую должность» в изменении условий ТД
Третий разобранный вид: новая должность из справочника с ручным вводом, дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное) и обоснование. Должностная инструкция по ТЗ прикладывается всегда — обязательный слот вложений. Работника и работодателя не различаем: причины видны всем. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,8 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
||||
| --- | --- | --- |
|
||||
| Повышение должностного оклада | `SalaryIncrease` | текущий оклад, новый оклад (`MoneyInput`, Number), дата изменения, причина (испытательный срок / расширение функционала / иное), обоснование — при «иное» это и есть текст причины |
|
||||
| Установление надбавки / доплаты | `Allowance` | основание (а–д: надбавка или доплата видно из пункта), дата начала, дата окончания или «бессрочно» (флажок прячет и очищает дату), размер + переключатель «% / ₸» (`UnitSwitch`, по умолчанию %), обоснование — в т.ч. объём доп. работы при совмещении и текст «иное». Письменное согласие работника на совмещение оформляет кадровик после согласования — не в форме. Вложение **«Результаты аттестации»** — только при основании «г», обязательно |
|
||||
| Остальные 8 | — | ещё не разобраны с кадрами |
|
||||
| Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно |
|
||||
| Остальные 7 | — | ещё не разобраны с кадрами |
|
||||
|
||||
**Вложения по колонке ТЗ «Что приложить»** (решение 2026-10-28): фиксированные
|
||||
подписанные слоты по виду (подпись — что за документ); обязательны там, где ТЗ
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ChangeType } from '../changeTypes';
|
||||
import { ISectionProps } from '../model';
|
||||
import { SalaryIncreaseSection } from './SalaryIncrease/SalaryIncreaseSection';
|
||||
import { AllowanceSection } from './Allowance/AllowanceSection';
|
||||
import { PositionTransferSection } from './PositionTransfer/PositionTransferSection';
|
||||
|
||||
/**
|
||||
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
||||
@@ -11,7 +12,8 @@ import { AllowanceSection } from './Allowance/AllowanceSection';
|
||||
*/
|
||||
const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
|
||||
'salary-increase': SalaryIncreaseSection,
|
||||
allowance: AllowanceSection
|
||||
allowance: AllowanceSection,
|
||||
'position-transfer': PositionTransferSection
|
||||
};
|
||||
|
||||
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextInput, TextArea, Select } from '../../../../shared/ui';
|
||||
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||
import { PositionPicker } from '../../../../shared/dictionary';
|
||||
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
||||
import { ISectionProps } from '../../model';
|
||||
import { POSITION_TRANSFER_REASONS, JOB_DESCRIPTION_ATTACHMENT } from './options';
|
||||
|
||||
/**
|
||||
* Поля вида «Перевод на другую должность (постоянный)». Состав согласован с
|
||||
* Тимуром по ТЗ кадров: новая должность (справочник с ручным вводом), дата,
|
||||
* причина (а–г), обоснование и обязательная должностная инструкция. Текущая
|
||||
* должность уже видна в «Организации» — отдельного «было» нет.
|
||||
*
|
||||
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
|
||||
*/
|
||||
export const PositionTransferSection: React.FC<ISectionProps> = ({ data, set, context, attachments }) => {
|
||||
const readOnly = useFormReadOnly();
|
||||
|
||||
return (
|
||||
<CollapseSection title={CHANGE_TYPE_LABELS['position-transfer']} icon="people">
|
||||
<FormField label="Новая должность" htmlFor="PositionTransferNewPosition">
|
||||
<PositionPicker
|
||||
context={context}
|
||||
id="PositionTransferNewPosition"
|
||||
value={data.PositionTransferNewPosition}
|
||||
onChange={(v) => set('PositionTransferNewPosition', v)}
|
||||
placeholder="Начните вводить название должности…"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Дата перевода" htmlFor="PositionTransferDate">
|
||||
<TextInput
|
||||
id="PositionTransferDate"
|
||||
type="date"
|
||||
value={data.PositionTransferDate}
|
||||
onChange={(v) => set('PositionTransferDate', v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Причина" htmlFor="PositionTransferReason" full>
|
||||
<Select
|
||||
id="PositionTransferReason"
|
||||
value={data.PositionTransferReason}
|
||||
onChange={(v) => set('PositionTransferReason', v)}
|
||||
options={POSITION_TRANSFER_REASONS}
|
||||
placeholder="— выберите —"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Обоснование" htmlFor="PositionTransferJustification" full>
|
||||
<TextArea
|
||||
id="PositionTransferJustification"
|
||||
value={data.PositionTransferJustification}
|
||||
onChange={(v) => set('PositionTransferJustification', v)}
|
||||
/>
|
||||
</FormField>
|
||||
{attachments && (
|
||||
<AttachmentSlotField
|
||||
label={JOB_DESCRIPTION_ATTACHMENT}
|
||||
editor={attachments}
|
||||
editable={!readOnly}
|
||||
groupKey={JOB_DESCRIPTION_ATTACHMENT}
|
||||
category={JOB_DESCRIPTION_ATTACHMENT}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</CollapseSection>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ISelectOption } from '../../../../shared/ui';
|
||||
|
||||
/**
|
||||
* Причины перевода на другую должность — по шаблону текста ТЗ кадров (а–г).
|
||||
* Работника и работодателя не различаем (решение 2026-10-28): причины видны
|
||||
* всем, работник при необходимости выбирает «Иное» и пишет текст в обоснование.
|
||||
* Значения совпадают с choices колонки PositionTransferReason (provisioning.json).
|
||||
*/
|
||||
export const POSITION_TRANSFER_REASONS: ISelectOption[] = [
|
||||
'Карьерный рост (успешное выполнение обязанностей, высокие результаты)',
|
||||
'Вакантная должность, соответствующая квалификации работника',
|
||||
'Открытие нового объекта / проекта, перераспределение функций',
|
||||
'Иное'
|
||||
].map((reason) => ({ value: reason, label: reason }));
|
||||
|
||||
/** Вложение «Должностная инструкция» — по ТЗ ПРИКЛАДЫВАЕТСЯ всегда, обязательно. */
|
||||
export const JOB_DESCRIPTION_ATTACHMENT = 'Должностная инструкция';
|
||||
@@ -2,6 +2,7 @@ import { PrepareFiles, IAttachmentEditor } from '../../../shared/attachments/use
|
||||
import { attachmentsOfCategory, prefixFiles } from '../../../shared/attachments/attachmentCategories';
|
||||
import { IFormData } from '../model';
|
||||
import { ATTESTATION_ATTACHMENT, requiresAttestation } from './Allowance/options';
|
||||
import { JOB_DESCRIPTION_ATTACHMENT } from './PositionTransfer/options';
|
||||
|
||||
/**
|
||||
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
|
||||
@@ -19,6 +20,9 @@ export function activeAttachmentSlots(data: IFormData): IAttachmentSlot[] {
|
||||
if (data.ChangeTypes.includes('allowance') && requiresAttestation(data.AllowanceReason)) {
|
||||
slots.push({ category: ATTESTATION_ATTACHMENT, required: true });
|
||||
}
|
||||
if (data.ChangeTypes.includes('position-transfer')) {
|
||||
slots.push({ category: JOB_DESCRIPTION_ATTACHMENT, required: true });
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,11 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
||||
'AllowanceIndefinite',
|
||||
'AllowanceAmount',
|
||||
'AllowanceUnit',
|
||||
'AllowanceJustification'
|
||||
'AllowanceJustification',
|
||||
'PositionTransferNewPosition',
|
||||
'PositionTransferDate',
|
||||
'PositionTransferReason',
|
||||
'PositionTransferJustification'
|
||||
)()) as Record<string, unknown>;
|
||||
|
||||
const data: IFormData = {
|
||||
@@ -118,7 +122,11 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
||||
AllowanceIndefinite: item.AllowanceIndefinite === true,
|
||||
AllowanceAmount: numberText(item.AllowanceAmount),
|
||||
AllowanceUnit: allowanceUnitFromText(text(item.AllowanceUnit)),
|
||||
AllowanceJustification: text(item.AllowanceJustification)
|
||||
AllowanceJustification: text(item.AllowanceJustification),
|
||||
PositionTransferNewPosition: text(item.PositionTransferNewPosition),
|
||||
PositionTransferDate: spDateOnlyToIso(text(item.PositionTransferDate)),
|
||||
PositionTransferReason: text(item.PositionTransferReason),
|
||||
PositionTransferJustification: text(item.PositionTransferJustification)
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -63,6 +63,17 @@ export interface IFormData {
|
||||
AllowanceUnit: AllowanceUnit;
|
||||
/** Обоснование; при совмещении — в т.ч. объём доп. работы, при «иное» — его текст. */
|
||||
AllowanceJustification: string;
|
||||
|
||||
// ── Вид «Перевод на другую должность (постоянный)» (byType/PositionTransfer).
|
||||
// Поля необязательные; вложение «Должностная инструкция» — обязательно (attachmentSlots).
|
||||
/** Новая должность — из справочника или вручную (PositionPicker). */
|
||||
PositionTransferNewPosition: string;
|
||||
/** С какой даты, ISO YYYY-MM-DD. */
|
||||
PositionTransferDate: string;
|
||||
/** Причина — одно из POSITION_TRANSFER_REASONS (Choice-колонка); '' — не выбрана. */
|
||||
PositionTransferReason: string;
|
||||
/** Обоснование; при «Иное» — его текст, у работника — текст заявления. */
|
||||
PositionTransferJustification: string;
|
||||
}
|
||||
|
||||
/** Обновление одного поля формы с сохранением типа значения. */
|
||||
@@ -91,7 +102,11 @@ export const EMPTY_DATA: IFormData = {
|
||||
AllowanceIndefinite: false,
|
||||
AllowanceAmount: '',
|
||||
AllowanceUnit: DEFAULT_ALLOWANCE_UNIT,
|
||||
AllowanceJustification: ''
|
||||
AllowanceJustification: '',
|
||||
PositionTransferNewPosition: '',
|
||||
PositionTransferDate: '',
|
||||
PositionTransferReason: '',
|
||||
PositionTransferJustification: ''
|
||||
};
|
||||
|
||||
/** Плейсхолдер для поля выбора сотрудника. */
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
{ "internalName": "AllowanceIndefinite", "title": "Надбавка/доплата: бессрочно", "type": "Boolean" },
|
||||
{ "internalName": "AllowanceAmount", "title": "Надбавка/доплата: размер", "type": "Number", "decimals": 2 },
|
||||
{ "internalName": "AllowanceUnit", "title": "Надбавка/доплата: единица размера", "type": "Choice", "choices": ["%", "₸"] },
|
||||
{ "internalName": "AllowanceJustification", "title": "Надбавка/доплата: обоснование", "type": "Note" }
|
||||
{ "internalName": "AllowanceJustification", "title": "Надбавка/доплата: обоснование", "type": "Note" },
|
||||
|
||||
{ "internalName": "PositionTransferNewPosition", "title": "Перевод на должность: новая должность", "type": "Text" },
|
||||
{ "internalName": "PositionTransferDate", "title": "Перевод на должность: дата перевода", "type": "DateTime", "dateOnly": true },
|
||||
{ "internalName": "PositionTransferReason", "title": "Перевод на должность: причина", "type": "Choice", "choices": [
|
||||
"Карьерный рост (успешное выполнение обязанностей, высокие результаты)",
|
||||
"Вакантная должность, соответствующая квалификации работника",
|
||||
"Открытие нового объекта / проекта, перераспределение функций",
|
||||
"Иное"
|
||||
] },
|
||||
{ "internalName": "PositionTransferJustification", "title": "Перевод на должность: обоснование", "type": "Note" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -95,6 +95,24 @@ function allowanceColumns(data: IFormData): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
/** Колонки вида «Перевод на другую должность»; вид не выбран — очищаются (см. salaryIncreaseColumns). */
|
||||
function positionTransferColumns(data: IFormData): Record<string, unknown> {
|
||||
if (!data.ChangeTypes.includes('position-transfer')) {
|
||||
return {
|
||||
PositionTransferNewPosition: null,
|
||||
PositionTransferDate: null,
|
||||
PositionTransferReason: null,
|
||||
PositionTransferJustification: null
|
||||
};
|
||||
}
|
||||
return {
|
||||
PositionTransferNewPosition: data.PositionTransferNewPosition.trim() || null,
|
||||
PositionTransferDate: data.PositionTransferDate || null,
|
||||
PositionTransferReason: data.PositionTransferReason || null,
|
||||
PositionTransferJustification: data.PositionTransferJustification || null
|
||||
};
|
||||
}
|
||||
|
||||
/** Поля элемента списка данных из модели формы. */
|
||||
function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||
return {
|
||||
@@ -104,7 +122,8 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||
// MultiChoice пишется плоским массивом подписей (как «База 1С» в Ticket1C); [] очищает колонку.
|
||||
ChangeTypes: data.ChangeTypes.map((type) => CHANGE_TYPE_LABELS[type]),
|
||||
...salaryIncreaseColumns(data),
|
||||
...allowanceColumns(data)
|
||||
...allowanceColumns(data),
|
||||
...positionTransferColumns(data)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user