feat: поля вида «Повышение должностного оклада» в изменении условий ТД
Первый разобранный с кадрами вид: текущий и новый оклад, дата, причина (испытательный срок / расширение функционала / иное) и обоснование — оно же текст причины «иное». Всё необязательно. Поля вида живут в своей секции byType/, а колонки невыбранного вида при сохранении очищаются, чтобы после смены вида в заявке не оставалось скрытых значений. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ import { saveContractChangeRequest, contractChangeApprovalService } from './subm
|
|||||||
import { loadContractChangeRequest } from './load';
|
import { loadContractChangeRequest } from './load';
|
||||||
import { useSelfEmployee } from './useSelfEmployee';
|
import { useSelfEmployee } from './useSelfEmployee';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
|
import { ChangeTypeSections } from './byType/ChangeTypeSections';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
|
|
||||||
const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
||||||
@@ -258,6 +259,7 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<RequestSection data={data} set={set} context={context} />
|
<RequestSection data={data} set={set} context={context} />
|
||||||
|
<ChangeTypeSections data={data} set={set} context={context} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|
||||||
<ApprovalMatrix
|
<ApprovalMatrix
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
|||||||
import { loadContractChangeRequest } from './load';
|
import { loadContractChangeRequest } from './load';
|
||||||
import { contractChangeApprovalService } from './submit';
|
import { contractChangeApprovalService } from './submit';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
|
import { ChangeTypeSections } from './byType/ChangeTypeSections';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
|
|
||||||
interface IContractChangeViewProps {
|
interface IContractChangeViewProps {
|
||||||
@@ -137,6 +138,7 @@ const ContractChangeView: React.FC<IContractChangeViewProps> = ({ context, itemI
|
|||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
<RequestSection data={data} set={NOOP_SET} context={context} />
|
<RequestSection data={data} set={NOOP_SET} context={context} />
|
||||||
|
<ChangeTypeSections data={data} set={NOOP_SET} context={context} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,24 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
|||||||
менять только вместе с `provisioning.json`.
|
менять только вместе с `provisioning.json`.
|
||||||
- **Заголовок элемента** (`Title`): `Изменение условий ТД — {ФИО} — дд.мм.гггг`.
|
- **Заголовок элемента** (`Title`): `Изменение условий ТД — {ФИО} — дд.мм.гггг`.
|
||||||
|
|
||||||
|
## Поля по видам
|
||||||
|
Каждый вид — своя секция в `byType/<Вид>/`, подключается строкой в
|
||||||
|
`byType/ChangeTypeSections.tsx`; ключи модели и колонки SP — с префиксом вида.
|
||||||
|
Колонки невыбранного вида при сохранении очищаются (`null`), чтобы в заявке не
|
||||||
|
оставалось скрытых значений после смены вида.
|
||||||
|
|
||||||
|
| Вид | Секция | Поля (все необязательные, без ограничений) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Повышение должностного оклада | `SalaryIncrease` | текущий оклад, новый оклад (`MoneyInput`, Number), дата изменения, причина (испытательный срок / расширение функционала / иное), обоснование — при «иное» это и есть текст причины |
|
||||||
|
| Остальные 9 | — | ещё не разобраны с кадрами |
|
||||||
|
|
||||||
## Открытые вопросы (проверить на тесте)
|
## Открытые вопросы (проверить на тесте)
|
||||||
- Выдаёт ли бэкенд (вебхуки) права на элемент по People-колонке `Employee`? Если да —
|
- Выдаёт ли бэкенд (вебхуки) права на элемент по People-колонке `Employee`? Если да —
|
||||||
сотрудник, за которого подали заявку, её не увидит (такой колонки здесь нет).
|
сотрудник, за которого подали заявку, её не увидит (такой колонки здесь нет).
|
||||||
- `Employee/FullBase`: ищет ли `Fio` по подстроке; что фильтрует `Status`.
|
- `Employee/FullBase`: ищет ли `Fio` по подстроке; что фильтрует `Status`.
|
||||||
|
|
||||||
## Секции формы
|
## Секции формы
|
||||||
RequestSection (сотрудник · вид изменения)
|
RequestSection (сотрудник · вид изменения) · ChangeTypeSections (секция выбранного вида, см. «Поля по видам»)
|
||||||
|
|
||||||
## Точки входа
|
## Точки входа
|
||||||
- rptype: `hr-contract-change`
|
- rptype: `hr-contract-change`
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ChangeType } from '../changeTypes';
|
||||||
|
import { ISectionProps } from '../model';
|
||||||
|
import { SalaryIncreaseSection } from './SalaryIncrease/SalaryIncreaseSection';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
||||||
|
* согласования с кадрами; у вида без секции под выбором просто ничего не появится.
|
||||||
|
* Новый вид = папка byType/<Вид>/ + строка здесь.
|
||||||
|
*/
|
||||||
|
const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
|
||||||
|
'salary-increase': SalaryIncreaseSection
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
||||||
|
export const ChangeTypeSections: React.FC<ISectionProps> = (props) => (
|
||||||
|
<>
|
||||||
|
{props.data.ChangeTypes.map((type) => {
|
||||||
|
const Section = SECTION_BY_TYPE[type];
|
||||||
|
return Section ? <Section key={type} {...props} /> : undefined;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { CollapseSection, FormField, MoneyInput, TextInput, TextArea, Select } from '../../../../shared/ui';
|
||||||
|
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
||||||
|
import { ISectionProps } from '../../model';
|
||||||
|
import { SALARY_INCREASE_REASONS } from './reasons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поля вида «Повышение должностного оклада». Состав согласован с Тимуром по ТЗ
|
||||||
|
* кадров: было/стало, дата, причина (а/б/в) и обоснование. Все необязательные,
|
||||||
|
* без ограничений — по решению от 2026-10-28.
|
||||||
|
*
|
||||||
|
* Поля — прямо в CollapseSection, без FormGrid: тело секции само является
|
||||||
|
* двухколоночной сеткой, вложенная FormGrid сжала бы всё в одну её ячейку.
|
||||||
|
*/
|
||||||
|
export const SalaryIncreaseSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||||
|
<CollapseSection title={CHANGE_TYPE_LABELS['salary-increase']} icon="calculator">
|
||||||
|
<FormField label="Текущий оклад, ₸" htmlFor="SalaryIncreaseCurrent">
|
||||||
|
<MoneyInput
|
||||||
|
id="SalaryIncreaseCurrent"
|
||||||
|
value={data.SalaryIncreaseCurrent}
|
||||||
|
onChange={(v) => set('SalaryIncreaseCurrent', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Новый оклад, ₸" htmlFor="SalaryIncreaseNew">
|
||||||
|
<MoneyInput
|
||||||
|
id="SalaryIncreaseNew"
|
||||||
|
value={data.SalaryIncreaseNew}
|
||||||
|
onChange={(v) => set('SalaryIncreaseNew', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Дата изменения" htmlFor="SalaryIncreaseDate">
|
||||||
|
<TextInput
|
||||||
|
id="SalaryIncreaseDate"
|
||||||
|
type="date"
|
||||||
|
value={data.SalaryIncreaseDate}
|
||||||
|
onChange={(v) => set('SalaryIncreaseDate', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Причина" htmlFor="SalaryIncreaseReason">
|
||||||
|
<Select
|
||||||
|
id="SalaryIncreaseReason"
|
||||||
|
value={data.SalaryIncreaseReason}
|
||||||
|
onChange={(v) => set('SalaryIncreaseReason', v)}
|
||||||
|
options={SALARY_INCREASE_REASONS}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Обоснование" htmlFor="SalaryIncreaseJustification" full>
|
||||||
|
<TextArea
|
||||||
|
id="SalaryIncreaseJustification"
|
||||||
|
value={data.SalaryIncreaseJustification}
|
||||||
|
onChange={(v) => set('SalaryIncreaseJustification', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</CollapseSection>
|
||||||
|
);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ISelectOption } from '../../../../shared/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Причины повышения оклада — по шаблону текста ТЗ кадров (а/б/в). Значения
|
||||||
|
* совпадают с choices колонки SalaryIncreaseReason (provisioning.json).
|
||||||
|
* «Иное» отдельного текста не имеет — его раскрывает поле «Обоснование».
|
||||||
|
*/
|
||||||
|
export const SALARY_REASON_PROBATION = 'Успешное прохождение испытательного срока';
|
||||||
|
export const SALARY_REASON_RESPONSIBILITY = 'Расширение функционала / зоны ответственности';
|
||||||
|
export const SALARY_REASON_OTHER = 'Иное';
|
||||||
|
|
||||||
|
export const SALARY_INCREASE_REASONS: ISelectOption[] = [
|
||||||
|
{ value: SALARY_REASON_PROBATION, label: SALARY_REASON_PROBATION },
|
||||||
|
{ value: SALARY_REASON_RESPONSIBILITY, label: SALARY_REASON_RESPONSIBILITY },
|
||||||
|
{ value: SALARY_REASON_OTHER, label: SALARY_REASON_OTHER }
|
||||||
|
];
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||||
import { ChangeType, changeTypeFromLabel } from './changeTypes';
|
import { ChangeType, changeTypeFromLabel } from './changeTypes';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
@@ -15,6 +16,11 @@ function text(raw: unknown): string {
|
|||||||
return typeof raw === 'string' ? raw : '';
|
return typeof raw === 'string' ? raw : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Number-колонка → «сырые» цифры для MoneyInput; пусто → ''. */
|
||||||
|
function moneyText(raw: unknown): string {
|
||||||
|
return typeof raw === 'number' && Number.isFinite(raw) ? String(raw) : '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Колонки Employee* → запись сотрудника (см. employeeColumns в submit.ts).
|
* Колонки Employee* → запись сотрудника (см. employeeColumns в submit.ts).
|
||||||
* Нет ни id, ни Ф.И.О. — сотрудник не был выбран (черновик) → undefined.
|
* Нет ни id, ни Ф.И.О. — сотрудник не был выбран (черновик) → undefined.
|
||||||
@@ -80,13 +86,24 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
'EmployeeCompany',
|
'EmployeeCompany',
|
||||||
'EmployeePosition',
|
'EmployeePosition',
|
||||||
'EmployeeDepartment',
|
'EmployeeDepartment',
|
||||||
'ChangeTypes'
|
'ChangeTypes',
|
||||||
|
'SalaryIncreaseCurrent',
|
||||||
|
'SalaryIncreaseNew',
|
||||||
|
'SalaryIncreaseDate',
|
||||||
|
'SalaryIncreaseReason',
|
||||||
|
'SalaryIncreaseJustification'
|
||||||
)()) as Record<string, unknown>;
|
)()) as Record<string, unknown>;
|
||||||
|
|
||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
Employee: employeeFromItem(item),
|
Employee: employeeFromItem(item),
|
||||||
ChangeTypes: changeTypesFromItem(item.ChangeTypes)
|
ChangeTypes: changeTypesFromItem(item.ChangeTypes),
|
||||||
|
SalaryIncreaseCurrent: moneyText(item.SalaryIncreaseCurrent),
|
||||||
|
SalaryIncreaseNew: moneyText(item.SalaryIncreaseNew),
|
||||||
|
// Дату только через spDateOnlyToIso — иначе из-за часового пояса уезжает на день назад.
|
||||||
|
SalaryIncreaseDate: spDateOnlyToIso(text(item.SalaryIncreaseDate)),
|
||||||
|
SalaryIncreaseReason: text(item.SalaryIncreaseReason),
|
||||||
|
SalaryIncreaseJustification: text(item.SalaryIncreaseJustification)
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ export interface IFormData {
|
|||||||
* задел под несколько, см. changeTypes.ts. [] — не выбран.
|
* задел под несколько, см. changeTypes.ts. [] — не выбран.
|
||||||
*/
|
*/
|
||||||
ChangeTypes: ChangeType[];
|
ChangeTypes: ChangeType[];
|
||||||
|
|
||||||
|
// ── Вид «Повышение должностного оклада» (byType/SalaryIncrease). Все необязательные.
|
||||||
|
/** Текущий оклад — «сырые» цифры без пробелов (так отдаёт MoneyInput); '' — не заполнен. */
|
||||||
|
SalaryIncreaseCurrent: string;
|
||||||
|
/** Новый оклад — как SalaryIncreaseCurrent. */
|
||||||
|
SalaryIncreaseNew: string;
|
||||||
|
/** С какой даты, ISO YYYY-MM-DD. */
|
||||||
|
SalaryIncreaseDate: string;
|
||||||
|
/** Причина — одно из SALARY_INCREASE_REASONS (Choice-колонка); '' — не выбрана. */
|
||||||
|
SalaryIncreaseReason: string;
|
||||||
|
/** Обоснование; при причине «Иное» — её текст. */
|
||||||
|
SalaryIncreaseJustification: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновление одного поля формы с сохранением типа значения. */
|
/** Обновление одного поля формы с сохранением типа значения. */
|
||||||
@@ -42,7 +54,12 @@ export interface ISectionProps {
|
|||||||
|
|
||||||
export const EMPTY_DATA: IFormData = {
|
export const EMPTY_DATA: IFormData = {
|
||||||
Employee: undefined,
|
Employee: undefined,
|
||||||
ChangeTypes: []
|
ChangeTypes: [],
|
||||||
|
SalaryIncreaseCurrent: '',
|
||||||
|
SalaryIncreaseNew: '',
|
||||||
|
SalaryIncreaseDate: '',
|
||||||
|
SalaryIncreaseReason: '',
|
||||||
|
SalaryIncreaseJustification: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Плейсхолдер для поля выбора сотрудника. */
|
/** Плейсхолдер для поля выбора сотрудника. */
|
||||||
|
|||||||
@@ -24,6 +24,16 @@
|
|||||||
"Изменение трудовой функции (без смены должности)",
|
"Изменение трудовой функции (без смены должности)",
|
||||||
"Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)",
|
"Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)",
|
||||||
"Временный перевод при производственной необходимости / простое"
|
"Временный перевод при производственной необходимости / простое"
|
||||||
] }
|
] },
|
||||||
|
|
||||||
|
{ "internalName": "SalaryIncreaseCurrent", "title": "Повышение оклада: текущий оклад", "type": "Number", "decimals": 0 },
|
||||||
|
{ "internalName": "SalaryIncreaseNew", "title": "Повышение оклада: новый оклад", "type": "Number", "decimals": 0 },
|
||||||
|
{ "internalName": "SalaryIncreaseDate", "title": "Повышение оклада: дата изменения", "type": "DateTime", "dateOnly": true },
|
||||||
|
{ "internalName": "SalaryIncreaseReason", "title": "Повышение оклада: причина", "type": "Choice", "choices": [
|
||||||
|
"Успешное прохождение испытательного срока",
|
||||||
|
"Расширение функционала / зоны ответственности",
|
||||||
|
"Иное"
|
||||||
|
] },
|
||||||
|
{ "internalName": "SalaryIncreaseJustification", "title": "Повышение оклада: обоснование", "type": "Note" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,35 @@ function employeeColumns(employee: IEmployeeFullBaseRecord | undefined): Record<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** «Сырые» цифры MoneyInput → число для Number-колонки; '' или мусор → undefined (не заполнено). */
|
||||||
|
function moneyColumn(raw: string): number | undefined {
|
||||||
|
const amount = Number(raw);
|
||||||
|
return raw.trim() !== '' && Number.isFinite(amount) ? amount : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Колонки вида «Повышение оклада». Вид не выбран — колонки очищаются: иначе
|
||||||
|
* после смены вида в заявке остались бы скрытые поля прежнего, которых никто не видит.
|
||||||
|
*/
|
||||||
|
function salaryIncreaseColumns(data: IFormData): Record<string, unknown> {
|
||||||
|
if (!data.ChangeTypes.includes('salary-increase')) {
|
||||||
|
return {
|
||||||
|
SalaryIncreaseCurrent: null,
|
||||||
|
SalaryIncreaseNew: null,
|
||||||
|
SalaryIncreaseDate: null,
|
||||||
|
SalaryIncreaseReason: null,
|
||||||
|
SalaryIncreaseJustification: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
SalaryIncreaseCurrent: moneyColumn(data.SalaryIncreaseCurrent) ?? null,
|
||||||
|
SalaryIncreaseNew: moneyColumn(data.SalaryIncreaseNew) ?? null,
|
||||||
|
SalaryIncreaseDate: data.SalaryIncreaseDate || null,
|
||||||
|
SalaryIncreaseReason: data.SalaryIncreaseReason || null,
|
||||||
|
SalaryIncreaseJustification: data.SalaryIncreaseJustification || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Поля элемента списка данных из модели формы. */
|
/** Поля элемента списка данных из модели формы. */
|
||||||
function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
@@ -48,7 +77,8 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
|||||||
RequestStatus: status,
|
RequestStatus: status,
|
||||||
...employeeColumns(data.Employee),
|
...employeeColumns(data.Employee),
|
||||||
// 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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user