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), дата изменения, причина (испытательный срок / расширение функционала / иное), обоснование — при «иное» это и есть текст причины |
|
| Повышение должностного оклада | `SalaryIncrease` | текущий оклад, новый оклад (`MoneyInput`, Number), дата изменения, причина (испытательный срок / расширение функционала / иное), обоснование — при «иное» это и есть текст причины |
|
||||||
| Установление надбавки / доплаты | `Allowance` | основание (а–д: надбавка или доплата видно из пункта), дата начала, дата окончания или «бессрочно» (флажок прячет и очищает дату), размер + переключатель «% / ₸» (`UnitSwitch`, по умолчанию %), обоснование — в т.ч. объём доп. работы при совмещении и текст «иное». Письменное согласие работника на совмещение оформляет кадровик после согласования — не в форме. Вложение **«Результаты аттестации»** — только при основании «г», обязательно |
|
| Установление надбавки / доплаты | `Allowance` | основание (а–д: надбавка или доплата видно из пункта), дата начала, дата окончания или «бессрочно» (флажок прячет и очищает дату), размер + переключатель «% / ₸» (`UnitSwitch`, по умолчанию %), обоснование — в т.ч. объём доп. работы при совмещении и текст «иное». Письменное согласие работника на совмещение оформляет кадровик после согласования — не в форме. Вложение **«Результаты аттестации»** — только при основании «г», обязательно |
|
||||||
| Остальные 8 | — | ещё не разобраны с кадрами |
|
| Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно |
|
||||||
|
| Остальные 7 | — | ещё не разобраны с кадрами |
|
||||||
|
|
||||||
**Вложения по колонке ТЗ «Что приложить»** (решение 2026-10-28): фиксированные
|
**Вложения по колонке ТЗ «Что приложить»** (решение 2026-10-28): фиксированные
|
||||||
подписанные слоты по виду (подпись — что за документ); обязательны там, где ТЗ
|
подписанные слоты по виду (подпись — что за документ); обязательны там, где ТЗ
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ChangeType } from '../changeTypes';
|
|||||||
import { ISectionProps } from '../model';
|
import { ISectionProps } from '../model';
|
||||||
import { SalaryIncreaseSection } from './SalaryIncrease/SalaryIncreaseSection';
|
import { SalaryIncreaseSection } from './SalaryIncrease/SalaryIncreaseSection';
|
||||||
import { AllowanceSection } from './Allowance/AllowanceSection';
|
import { AllowanceSection } from './Allowance/AllowanceSection';
|
||||||
|
import { PositionTransferSection } from './PositionTransfer/PositionTransferSection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
||||||
@@ -11,7 +12,8 @@ import { AllowanceSection } from './Allowance/AllowanceSection';
|
|||||||
*/
|
*/
|
||||||
const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
|
const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
|
||||||
'salary-increase': SalaryIncreaseSection,
|
'salary-increase': SalaryIncreaseSection,
|
||||||
allowance: AllowanceSection
|
allowance: AllowanceSection,
|
||||||
|
'position-transfer': PositionTransferSection
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. 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 { attachmentsOfCategory, prefixFiles } from '../../../shared/attachments/attachmentCategories';
|
||||||
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
|
* Подписанный слот вложений по колонке ТЗ «Что приложить». Подпись (category)
|
||||||
@@ -19,6 +20,9 @@ export function activeAttachmentSlots(data: IFormData): IAttachmentSlot[] {
|
|||||||
if (data.ChangeTypes.includes('allowance') && requiresAttestation(data.AllowanceReason)) {
|
if (data.ChangeTypes.includes('allowance') && requiresAttestation(data.AllowanceReason)) {
|
||||||
slots.push({ category: ATTESTATION_ATTACHMENT, required: true });
|
slots.push({ category: ATTESTATION_ATTACHMENT, required: true });
|
||||||
}
|
}
|
||||||
|
if (data.ChangeTypes.includes('position-transfer')) {
|
||||||
|
slots.push({ category: JOB_DESCRIPTION_ATTACHMENT, required: true });
|
||||||
|
}
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,11 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
'AllowanceIndefinite',
|
'AllowanceIndefinite',
|
||||||
'AllowanceAmount',
|
'AllowanceAmount',
|
||||||
'AllowanceUnit',
|
'AllowanceUnit',
|
||||||
'AllowanceJustification'
|
'AllowanceJustification',
|
||||||
|
'PositionTransferNewPosition',
|
||||||
|
'PositionTransferDate',
|
||||||
|
'PositionTransferReason',
|
||||||
|
'PositionTransferJustification'
|
||||||
)()) as Record<string, unknown>;
|
)()) as Record<string, unknown>;
|
||||||
|
|
||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
@@ -118,7 +122,11 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
AllowanceIndefinite: item.AllowanceIndefinite === true,
|
AllowanceIndefinite: item.AllowanceIndefinite === true,
|
||||||
AllowanceAmount: numberText(item.AllowanceAmount),
|
AllowanceAmount: numberText(item.AllowanceAmount),
|
||||||
AllowanceUnit: allowanceUnitFromText(text(item.AllowanceUnit)),
|
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 {
|
return {
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ export interface IFormData {
|
|||||||
AllowanceUnit: AllowanceUnit;
|
AllowanceUnit: AllowanceUnit;
|
||||||
/** Обоснование; при совмещении — в т.ч. объём доп. работы, при «иное» — его текст. */
|
/** Обоснование; при совмещении — в т.ч. объём доп. работы, при «иное» — его текст. */
|
||||||
AllowanceJustification: string;
|
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,
|
AllowanceIndefinite: false,
|
||||||
AllowanceAmount: '',
|
AllowanceAmount: '',
|
||||||
AllowanceUnit: DEFAULT_ALLOWANCE_UNIT,
|
AllowanceUnit: DEFAULT_ALLOWANCE_UNIT,
|
||||||
AllowanceJustification: ''
|
AllowanceJustification: '',
|
||||||
|
PositionTransferNewPosition: '',
|
||||||
|
PositionTransferDate: '',
|
||||||
|
PositionTransferReason: '',
|
||||||
|
PositionTransferJustification: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Плейсхолдер для поля выбора сотрудника. */
|
/** Плейсхолдер для поля выбора сотрудника. */
|
||||||
|
|||||||
@@ -48,6 +48,16 @@
|
|||||||
{ "internalName": "AllowanceIndefinite", "title": "Надбавка/доплата: бессрочно", "type": "Boolean" },
|
{ "internalName": "AllowanceIndefinite", "title": "Надбавка/доплата: бессрочно", "type": "Boolean" },
|
||||||
{ "internalName": "AllowanceAmount", "title": "Надбавка/доплата: размер", "type": "Number", "decimals": 2 },
|
{ "internalName": "AllowanceAmount", "title": "Надбавка/доплата: размер", "type": "Number", "decimals": 2 },
|
||||||
{ "internalName": "AllowanceUnit", "title": "Надбавка/доплата: единица размера", "type": "Choice", "choices": ["%", "₸"] },
|
{ "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> {
|
function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
@@ -104,7 +122,8 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
|||||||
// MultiChoice пишется плоским массивом подписей (как «База 1С» в Ticket1C); [] очищает колонку.
|
// MultiChoice пишется плоским массивом подписей (как «База 1С» в Ticket1C); [] очищает колонку.
|
||||||
ChangeTypes: data.ChangeTypes.map((type) => CHANGE_TYPE_LABELS[type]),
|
ChangeTypes: data.ChangeTypes.map((type) => CHANGE_TYPE_LABELS[type]),
|
||||||
...salaryIncreaseColumns(data),
|
...salaryIncreaseColumns(data),
|
||||||
...allowanceColumns(data)
|
...allowanceColumns(data),
|
||||||
|
...positionTransferColumns(data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user