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 { useSelfEmployee } from './useSelfEmployee';
|
||||
import { RequestSection } from './sections/RequestSection';
|
||||
import { ChangeTypeSections } from './byType/ChangeTypeSections';
|
||||
import { useStableCallback } from '../../shared/useStableCallback';
|
||||
|
||||
const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
||||
@@ -258,6 +259,7 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
<FormModeProvider readOnly={readOnly}>
|
||||
<RequestSection data={data} set={set} context={context} />
|
||||
<ChangeTypeSections data={data} set={set} context={context} />
|
||||
</FormModeProvider>
|
||||
|
||||
<ApprovalMatrix
|
||||
|
||||
@@ -27,6 +27,7 @@ import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||
import { loadContractChangeRequest } from './load';
|
||||
import { contractChangeApprovalService } from './submit';
|
||||
import { RequestSection } from './sections/RequestSection';
|
||||
import { ChangeTypeSections } from './byType/ChangeTypeSections';
|
||||
import { useStableCallback } from '../../shared/useStableCallback';
|
||||
|
||||
interface IContractChangeViewProps {
|
||||
@@ -137,6 +138,7 @@ const ContractChangeView: React.FC<IContractChangeViewProps> = ({ context, itemI
|
||||
>
|
||||
<FormModeProvider readOnly>
|
||||
<RequestSection data={data} set={NOOP_SET} context={context} />
|
||||
<ChangeTypeSections data={data} set={NOOP_SET} context={context} />
|
||||
</FormModeProvider>
|
||||
</FormCard>
|
||||
|
||||
|
||||
@@ -39,13 +39,24 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
||||
менять только вместе с `provisioning.json`.
|
||||
- **Заголовок элемента** (`Title`): `Изменение условий ТД — {ФИО} — дд.мм.гггг`.
|
||||
|
||||
## Поля по видам
|
||||
Каждый вид — своя секция в `byType/<Вид>/`, подключается строкой в
|
||||
`byType/ChangeTypeSections.tsx`; ключи модели и колонки SP — с префиксом вида.
|
||||
Колонки невыбранного вида при сохранении очищаются (`null`), чтобы в заявке не
|
||||
оставалось скрытых значений после смены вида.
|
||||
|
||||
| Вид | Секция | Поля (все необязательные, без ограничений) |
|
||||
| --- | --- | --- |
|
||||
| Повышение должностного оклада | `SalaryIncrease` | текущий оклад, новый оклад (`MoneyInput`, Number), дата изменения, причина (испытательный срок / расширение функционала / иное), обоснование — при «иное» это и есть текст причины |
|
||||
| Остальные 9 | — | ещё не разобраны с кадрами |
|
||||
|
||||
## Открытые вопросы (проверить на тесте)
|
||||
- Выдаёт ли бэкенд (вебхуки) права на элемент по People-колонке `Employee`? Если да —
|
||||
сотрудник, за которого подали заявку, её не увидит (такой колонки здесь нет).
|
||||
- `Employee/FullBase`: ищет ли `Fio` по подстроке; что фильтрует `Status`.
|
||||
|
||||
## Секции формы
|
||||
RequestSection (сотрудник · вид изменения)
|
||||
RequestSection (сотрудник · вид изменения) · ChangeTypeSections (секция выбранного вида, см. «Поля по видам»)
|
||||
|
||||
## Точки входа
|
||||
- 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 { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||
import { ChangeType, changeTypeFromLabel } from './changeTypes';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
@@ -15,6 +16,11 @@ function text(raw: unknown): string {
|
||||
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).
|
||||
* Нет ни id, ни Ф.И.О. — сотрудник не был выбран (черновик) → undefined.
|
||||
@@ -80,13 +86,24 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
||||
'EmployeeCompany',
|
||||
'EmployeePosition',
|
||||
'EmployeeDepartment',
|
||||
'ChangeTypes'
|
||||
'ChangeTypes',
|
||||
'SalaryIncreaseCurrent',
|
||||
'SalaryIncreaseNew',
|
||||
'SalaryIncreaseDate',
|
||||
'SalaryIncreaseReason',
|
||||
'SalaryIncreaseJustification'
|
||||
)()) as Record<string, unknown>;
|
||||
|
||||
const data: IFormData = {
|
||||
...EMPTY_DATA,
|
||||
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 {
|
||||
|
||||
@@ -28,6 +28,18 @@ export interface IFormData {
|
||||
* задел под несколько, см. changeTypes.ts. [] — не выбран.
|
||||
*/
|
||||
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 = {
|
||||
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> {
|
||||
return {
|
||||
@@ -48,7 +77,8 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||
RequestStatus: status,
|
||||
...employeeColumns(data.Employee),
|
||||
// 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