feat: поля вида «Надбавка / доплата» в изменении условий ТД
Второй разобранный вид: основание (а–д из шаблона ТЗ — надбавка это или доплата, видно из пункта), срок с/по или «бессрочно», размер с переключателем «% / ₸» и обоснование, куда пишется и объём доп. работы при совмещении. Проценты принимают дробные значения (7,5 %), тенге — целые; при переходе на тенге дробная часть отбрасывается. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,13 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
|||||||
| Вид | Секция | Поля (все необязательные, без ограничений) |
|
| Вид | Секция | Поля (все необязательные, без ограничений) |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Повышение должностного оклада | `SalaryIncrease` | текущий оклад, новый оклад (`MoneyInput`, Number), дата изменения, причина (испытательный срок / расширение функционала / иное), обоснование — при «иное» это и есть текст причины |
|
| Повышение должностного оклада | `SalaryIncrease` | текущий оклад, новый оклад (`MoneyInput`, Number), дата изменения, причина (испытательный срок / расширение функционала / иное), обоснование — при «иное» это и есть текст причины |
|
||||||
| Остальные 9 | — | ещё не разобраны с кадрами |
|
| Установление надбавки / доплаты | `Allowance` | основание (а–д: надбавка или доплата видно из пункта), дата начала, дата окончания или «бессрочно» (флажок прячет и очищает дату), размер + переключатель «% / ₸» (`UnitSwitch`, по умолчанию %), обоснование — в т.ч. объём доп. работы при совмещении и текст «иное». Письменное согласие работника на совмещение оформляет кадровик после согласования — не в форме. Вложение «Результаты аттестации» (обязательно при основании «г») — следующим шагом |
|
||||||
|
| Остальные 8 | — | ещё не разобраны с кадрами |
|
||||||
|
|
||||||
|
**Вложения по колонке ТЗ «Что приложить»** (решение 2026-10-28): фиксированные
|
||||||
|
подписанные слоты по виду (подпись — что за документ); обязательны там, где ТЗ
|
||||||
|
прямо требует документ (вид 2 «г» — аттестация, вид 3 — ДИ, вид 8 — новая ДИ;
|
||||||
|
вид 9 — текст ИЛИ файл), «можно приложить» — необязательно (вид 7).
|
||||||
|
|
||||||
## Открытые вопросы (проверить на тесте)
|
## Открытые вопросы (проверить на тесте)
|
||||||
- Выдаёт ли бэкенд (вебхуки) права на элемент по People-колонке `Employee`? Если да —
|
- Выдаёт ли бэкенд (вебхуки) права на элемент по People-колонке `Employee`? Если да —
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
@import '../../../../../../styles/design-tokens';
|
||||||
|
|
||||||
|
// Сумма + переключатель «% / ₸» в одну строку: поле тянется, переключатель — по содержимому.
|
||||||
|
.amount {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: $sp-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amountInput {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { CollapseSection, FormField, MoneyInput, TextInput, TextArea, Select, Checkbox } from '../../../../shared/ui';
|
||||||
|
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
||||||
|
import { ISectionProps } from '../../model';
|
||||||
|
import { ALLOWANCE_REASONS } from './options';
|
||||||
|
import { UnitSwitch } from './UnitSwitch';
|
||||||
|
import { sanitizePercentInput, amountForUnit } from './amount';
|
||||||
|
import styles from './AllowanceSection.module.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поля вида «Установление надбавки / доплаты». Состав согласован с Тимуром по ТЗ
|
||||||
|
* кадров: основание (а–д), срок с/по или бессрочно, размер в % или ₸, обоснование
|
||||||
|
* (в т.ч. объём доп. работы при совмещении). Все необязательные.
|
||||||
|
*
|
||||||
|
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
|
||||||
|
*/
|
||||||
|
export const AllowanceSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||||
|
<CollapseSection title={CHANGE_TYPE_LABELS.allowance} icon="calculator">
|
||||||
|
<FormField label="Основание" htmlFor="AllowanceReason" full>
|
||||||
|
<Select
|
||||||
|
id="AllowanceReason"
|
||||||
|
value={data.AllowanceReason}
|
||||||
|
onChange={(v) => set('AllowanceReason', v)}
|
||||||
|
options={ALLOWANCE_REASONS}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Дата начала" htmlFor="AllowanceStartDate">
|
||||||
|
<TextInput
|
||||||
|
id="AllowanceStartDate"
|
||||||
|
type="date"
|
||||||
|
value={data.AllowanceStartDate}
|
||||||
|
onChange={(v) => set('AllowanceStartDate', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Дата окончания" htmlFor="AllowanceEndDate">
|
||||||
|
{/* «Бессрочно» прячет дату окончания — и очищает её, чтобы не сохранить скрытое значение. */}
|
||||||
|
{!data.AllowanceIndefinite && (
|
||||||
|
<TextInput
|
||||||
|
id="AllowanceEndDate"
|
||||||
|
type="date"
|
||||||
|
value={data.AllowanceEndDate}
|
||||||
|
onChange={(v) => set('AllowanceEndDate', v)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Checkbox
|
||||||
|
checked={data.AllowanceIndefinite}
|
||||||
|
onChange={(checked) => {
|
||||||
|
set('AllowanceIndefinite', checked);
|
||||||
|
if (checked) set('AllowanceEndDate', '');
|
||||||
|
}}
|
||||||
|
label="Бессрочно"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Размер" htmlFor="AllowanceAmount">
|
||||||
|
<div className={styles.amount}>
|
||||||
|
<div className={styles.amountInput}>
|
||||||
|
{/* Проценты бывают дробными (7,5 %), тенге — целые с разделителем тысяч. */}
|
||||||
|
{data.AllowanceUnit === '%' ? (
|
||||||
|
<TextInput
|
||||||
|
id="AllowanceAmount"
|
||||||
|
value={data.AllowanceAmount}
|
||||||
|
onChange={(v) => set('AllowanceAmount', sanitizePercentInput(v))}
|
||||||
|
placeholder="например, 7.5"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MoneyInput id="AllowanceAmount" value={data.AllowanceAmount} onChange={(v) => set('AllowanceAmount', v)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<UnitSwitch
|
||||||
|
value={data.AllowanceUnit}
|
||||||
|
onChange={(unit) => {
|
||||||
|
set('AllowanceUnit', unit);
|
||||||
|
set('AllowanceAmount', amountForUnit(data.AllowanceAmount, unit));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Обоснование" htmlFor="AllowanceJustification" full>
|
||||||
|
<TextArea
|
||||||
|
id="AllowanceJustification"
|
||||||
|
value={data.AllowanceJustification}
|
||||||
|
onChange={(v) => set('AllowanceJustification', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</CollapseSection>
|
||||||
|
);
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
@import '../../../../../../styles/design-tokens';
|
||||||
|
|
||||||
|
// Две кнопки впритык, как сегментный контрол; высота — как у полей ввода рядом.
|
||||||
|
.switch {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid $gray-300;
|
||||||
|
border-radius: $r-sm;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
min-width: 44px;
|
||||||
|
padding: 0 $sp-3;
|
||||||
|
border: 0;
|
||||||
|
background: $white;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: $gray-700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .15s;
|
||||||
|
|
||||||
|
& + & {
|
||||||
|
border-left: 1px solid $gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active,
|
||||||
|
.active:hover:not(:disabled) {
|
||||||
|
background: $navy-700;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||||
|
import { AllowanceUnit, ALLOWANCE_UNITS } from './options';
|
||||||
|
import styles from './UnitSwitch.module.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: AllowanceUnit;
|
||||||
|
onChange: (unit: AllowanceUnit) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключатель единицы размера «% / ₸» рядом с полем суммы. Сегментного
|
||||||
|
* контрола в ui-kit нет; если понадобится ещё где-то — перенести туда.
|
||||||
|
* В режиме чтения кнопки неактивны, выбранная остаётся подсвеченной.
|
||||||
|
*/
|
||||||
|
export const UnitSwitch: React.FC<Props> = ({ value, onChange }) => {
|
||||||
|
const readOnly = useFormReadOnly();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.switch} role="radiogroup" aria-label="Единица размера">
|
||||||
|
{ALLOWANCE_UNITS.map((unit) => (
|
||||||
|
<button
|
||||||
|
key={unit}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={unit === value}
|
||||||
|
className={unit === value ? `${styles.option} ${styles.active}` : styles.option}
|
||||||
|
disabled={readOnly}
|
||||||
|
onClick={() => onChange(unit)}
|
||||||
|
>
|
||||||
|
{unit}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { sanitizePercentInput, amountForUnit } from './amount';
|
||||||
|
|
||||||
|
describe('sanitizePercentInput', () => {
|
||||||
|
it('оставляет целое как есть', () => {
|
||||||
|
expect(sanitizePercentInput('10')).toBe('10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('запятую превращает в точку', () => {
|
||||||
|
expect(sanitizePercentInput('7,5')).toBe('7.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('выбрасывает буквы, пробелы и знак процента', () => {
|
||||||
|
expect(sanitizePercentInput(' 1a2 %')).toBe('12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('оставляет только первый разделитель и не больше двух знаков дроби', () => {
|
||||||
|
expect(sanitizePercentInput('1.2.345')).toBe('1.23');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('разрешает разделитель в конце — человек ещё вводит дробь', () => {
|
||||||
|
expect(sanitizePercentInput('7,')).toBe('7.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('amountForUnit', () => {
|
||||||
|
it('при переходе в тенге отбрасывает дробную часть', () => {
|
||||||
|
expect(amountForUnit('7.5', '₸')).toBe('7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('в процентах значение не меняет', () => {
|
||||||
|
expect(amountForUnit('7.5', '%')).toBe('7.5');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { AllowanceUnit } from './options';
|
||||||
|
|
||||||
|
/** Знаков после разделителя у процента (7,25 %). */
|
||||||
|
const PERCENT_FRACTION_DIGITS = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ввод процента → «сырое» число строкой с точкой: «7,5» → «7.5». Пропускаем
|
||||||
|
* только цифры и один разделитель (запятая или точка), дробь — до 2 знаков.
|
||||||
|
* Не type=number: у TextInput нет step, и браузер считал бы 7.5 невалидным при шаге 1.
|
||||||
|
*/
|
||||||
|
export function sanitizePercentInput(text: string): string {
|
||||||
|
const normalized = text.replace(/,/g, '.').replace(/[^\d.]/g, '');
|
||||||
|
const [whole, ...rest] = normalized.split('.');
|
||||||
|
if (rest.length === 0) return whole;
|
||||||
|
return `${whole}.${rest.join('').slice(0, PERCENT_FRACTION_DIGITS)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значение суммы после смены единицы: в тенге сумма целая (MoneyInput),
|
||||||
|
* поэтому дробную часть процента отбрасываем, а не показываем «75» из «7.5».
|
||||||
|
*/
|
||||||
|
export function amountForUnit(amount: string, unit: AllowanceUnit): string {
|
||||||
|
if (unit === '₸') return amount.split('.')[0];
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ISelectOption } from '../../../../shared/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Основания надбавки/доплаты — по шаблону текста ТЗ кадров (а–д). Что это —
|
||||||
|
* надбавка или доплата — видно из самого пункта, отдельного поля нет. Значения
|
||||||
|
* совпадают с choices колонки AllowanceReason (provisioning.json).
|
||||||
|
*/
|
||||||
|
export const ALLOWANCE_REASON_MENTORING = 'Надбавка: наставничество и обучение новых работников';
|
||||||
|
export const ALLOWANCE_REASON_TRAVEL = 'Надбавка: разъездной характер работы';
|
||||||
|
export const ALLOWANCE_REASON_COMBINING = 'Доплата: совмещение должностей / расширение зоны обслуживания';
|
||||||
|
export const ALLOWANCE_REASON_HAZARDOUS = 'Доплата: работа в тяжёлых, вредных и (или) опасных условиях труда';
|
||||||
|
export const ALLOWANCE_REASON_OTHER = 'Доплата: иное по решению первого руководителя';
|
||||||
|
|
||||||
|
export const ALLOWANCE_REASONS: ISelectOption[] = [
|
||||||
|
ALLOWANCE_REASON_MENTORING,
|
||||||
|
ALLOWANCE_REASON_TRAVEL,
|
||||||
|
ALLOWANCE_REASON_COMBINING,
|
||||||
|
ALLOWANCE_REASON_HAZARDOUS,
|
||||||
|
ALLOWANCE_REASON_OTHER
|
||||||
|
].map((reason) => ({ value: reason, label: reason }));
|
||||||
|
|
||||||
|
/** Единица размера; значения = choices колонки AllowanceUnit. */
|
||||||
|
export type AllowanceUnit = '%' | '₸';
|
||||||
|
export const ALLOWANCE_UNITS: AllowanceUnit[] = ['%', '₸'];
|
||||||
|
/** По умолчанию — проценты: пример в ТЗ «Надбавка 10%». */
|
||||||
|
export const DEFAULT_ALLOWANCE_UNIT: AllowanceUnit = '%';
|
||||||
|
|
||||||
|
/** Строка из SP → единица; неизвестное/пустое — единица по умолчанию. */
|
||||||
|
export function allowanceUnitFromText(text: string): AllowanceUnit {
|
||||||
|
return ALLOWANCE_UNITS.find((unit) => unit === text) ?? DEFAULT_ALLOWANCE_UNIT;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import { ChangeType } from '../changeTypes';
|
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
||||||
@@ -9,7 +10,8 @@ import { SalaryIncreaseSection } from './SalaryIncrease/SalaryIncreaseSection';
|
|||||||
* Новый вид = папка byType/<Вид>/ + строка здесь.
|
* Новый вид = папка byType/<Вид>/ + строка здесь.
|
||||||
*/
|
*/
|
||||||
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
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { dataListTitle } from '../../shared/listTitles';
|
|||||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
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 { allowanceUnitFromText } from './byType/Allowance/options';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -16,8 +17,8 @@ function text(raw: unknown): string {
|
|||||||
return typeof raw === 'string' ? raw : '';
|
return typeof raw === 'string' ? raw : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Number-колонка → «сырые» цифры для MoneyInput; пусто → ''. */
|
/** Number-колонка → «сырое» число строкой (для MoneyInput / ввода процента); пусто → ''. */
|
||||||
function moneyText(raw: unknown): string {
|
function numberText(raw: unknown): string {
|
||||||
return typeof raw === 'number' && Number.isFinite(raw) ? String(raw) : '';
|
return typeof raw === 'number' && Number.isFinite(raw) ? String(raw) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,19 +92,33 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
'SalaryIncreaseNew',
|
'SalaryIncreaseNew',
|
||||||
'SalaryIncreaseDate',
|
'SalaryIncreaseDate',
|
||||||
'SalaryIncreaseReason',
|
'SalaryIncreaseReason',
|
||||||
'SalaryIncreaseJustification'
|
'SalaryIncreaseJustification',
|
||||||
|
'AllowanceReason',
|
||||||
|
'AllowanceStartDate',
|
||||||
|
'AllowanceEndDate',
|
||||||
|
'AllowanceIndefinite',
|
||||||
|
'AllowanceAmount',
|
||||||
|
'AllowanceUnit',
|
||||||
|
'AllowanceJustification'
|
||||||
)()) 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),
|
SalaryIncreaseCurrent: numberText(item.SalaryIncreaseCurrent),
|
||||||
SalaryIncreaseNew: moneyText(item.SalaryIncreaseNew),
|
SalaryIncreaseNew: numberText(item.SalaryIncreaseNew),
|
||||||
// Дату только через spDateOnlyToIso — иначе из-за часового пояса уезжает на день назад.
|
// Дату только через spDateOnlyToIso — иначе из-за часового пояса уезжает на день назад.
|
||||||
SalaryIncreaseDate: spDateOnlyToIso(text(item.SalaryIncreaseDate)),
|
SalaryIncreaseDate: spDateOnlyToIso(text(item.SalaryIncreaseDate)),
|
||||||
SalaryIncreaseReason: text(item.SalaryIncreaseReason),
|
SalaryIncreaseReason: text(item.SalaryIncreaseReason),
|
||||||
SalaryIncreaseJustification: text(item.SalaryIncreaseJustification)
|
SalaryIncreaseJustification: text(item.SalaryIncreaseJustification),
|
||||||
|
AllowanceReason: text(item.AllowanceReason),
|
||||||
|
AllowanceStartDate: spDateOnlyToIso(text(item.AllowanceStartDate)),
|
||||||
|
AllowanceEndDate: spDateOnlyToIso(text(item.AllowanceEndDate)),
|
||||||
|
AllowanceIndefinite: item.AllowanceIndefinite === true,
|
||||||
|
AllowanceAmount: numberText(item.AllowanceAmount),
|
||||||
|
AllowanceUnit: allowanceUnitFromText(text(item.AllowanceUnit)),
|
||||||
|
AllowanceJustification: text(item.AllowanceJustification)
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
|
|||||||
import { RequestTypeId } from '../../models/types';
|
import { RequestTypeId } from '../../models/types';
|
||||||
import { IEmployeeFullBasePerson, IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
import { IEmployeeFullBasePerson, IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||||
import { ChangeType } from './changeTypes';
|
import { ChangeType } from './changeTypes';
|
||||||
|
import { AllowanceUnit, DEFAULT_ALLOWANCE_UNIT } from './byType/Allowance/options';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
@@ -43,6 +44,24 @@ export interface IFormData {
|
|||||||
SalaryIncreaseReason: string;
|
SalaryIncreaseReason: string;
|
||||||
/** Обоснование; при причине «Иное» — её текст. */
|
/** Обоснование; при причине «Иное» — её текст. */
|
||||||
SalaryIncreaseJustification: string;
|
SalaryIncreaseJustification: string;
|
||||||
|
|
||||||
|
// ── Вид «Установление надбавки / доплаты» (byType/Allowance). Все необязательные.
|
||||||
|
/** Основание — одно из ALLOWANCE_REASONS (Choice-колонка); '' — не выбрано. */
|
||||||
|
AllowanceReason: string;
|
||||||
|
/** С какой даты, ISO YYYY-MM-DD. */
|
||||||
|
AllowanceStartDate: string;
|
||||||
|
/** По какую дату, ISO; при AllowanceIndefinite всегда ''. */
|
||||||
|
AllowanceEndDate: string;
|
||||||
|
/** Бессрочно — тогда даты окончания нет. */
|
||||||
|
AllowanceIndefinite: boolean;
|
||||||
|
/**
|
||||||
|
* Размер — «сырое» число строкой: в ₸ целое (MoneyInput), в % может быть дробным
|
||||||
|
* с точкой, «7.5» (см. byType/Allowance/amount.ts); единица — в AllowanceUnit.
|
||||||
|
*/
|
||||||
|
AllowanceAmount: string;
|
||||||
|
AllowanceUnit: AllowanceUnit;
|
||||||
|
/** Обоснование; при совмещении — в т.ч. объём доп. работы, при «иное» — его текст. */
|
||||||
|
AllowanceJustification: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновление одного поля формы с сохранением типа значения. */
|
/** Обновление одного поля формы с сохранением типа значения. */
|
||||||
@@ -62,7 +81,14 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
SalaryIncreaseNew: '',
|
SalaryIncreaseNew: '',
|
||||||
SalaryIncreaseDate: '',
|
SalaryIncreaseDate: '',
|
||||||
SalaryIncreaseReason: '',
|
SalaryIncreaseReason: '',
|
||||||
SalaryIncreaseJustification: ''
|
SalaryIncreaseJustification: '',
|
||||||
|
AllowanceReason: '',
|
||||||
|
AllowanceStartDate: '',
|
||||||
|
AllowanceEndDate: '',
|
||||||
|
AllowanceIndefinite: false,
|
||||||
|
AllowanceAmount: '',
|
||||||
|
AllowanceUnit: DEFAULT_ALLOWANCE_UNIT,
|
||||||
|
AllowanceJustification: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Плейсхолдер для поля выбора сотрудника. */
|
/** Плейсхолдер для поля выбора сотрудника. */
|
||||||
|
|||||||
@@ -34,6 +34,20 @@
|
|||||||
"Расширение функционала / зоны ответственности",
|
"Расширение функционала / зоны ответственности",
|
||||||
"Иное"
|
"Иное"
|
||||||
] },
|
] },
|
||||||
{ "internalName": "SalaryIncreaseJustification", "title": "Повышение оклада: обоснование", "type": "Note" }
|
{ "internalName": "SalaryIncreaseJustification", "title": "Повышение оклада: обоснование", "type": "Note" },
|
||||||
|
|
||||||
|
{ "internalName": "AllowanceReason", "title": "Надбавка/доплата: основание", "type": "Choice", "choices": [
|
||||||
|
"Надбавка: наставничество и обучение новых работников",
|
||||||
|
"Надбавка: разъездной характер работы",
|
||||||
|
"Доплата: совмещение должностей / расширение зоны обслуживания",
|
||||||
|
"Доплата: работа в тяжёлых, вредных и (или) опасных условиях труда",
|
||||||
|
"Доплата: иное по решению первого руководителя"
|
||||||
|
] },
|
||||||
|
{ "internalName": "AllowanceStartDate", "title": "Надбавка/доплата: дата начала", "type": "DateTime", "dateOnly": true },
|
||||||
|
{ "internalName": "AllowanceEndDate", "title": "Надбавка/доплата: дата окончания", "type": "DateTime", "dateOnly": true },
|
||||||
|
{ "internalName": "AllowanceIndefinite", "title": "Надбавка/доплата: бессрочно", "type": "Boolean" },
|
||||||
|
{ "internalName": "AllowanceAmount", "title": "Надбавка/доплата: размер", "type": "Number", "decimals": 2 },
|
||||||
|
{ "internalName": "AllowanceUnit", "title": "Надбавка/доплата: единица размера", "type": "Choice", "choices": ["%", "₸"] },
|
||||||
|
{ "internalName": "AllowanceJustification", "title": "Надбавка/доплата: обоснование", "type": "Note" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ function employeeColumns(employee: IEmployeeFullBaseRecord | undefined): Record<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** «Сырые» цифры MoneyInput → число для Number-колонки; '' или мусор → undefined (не заполнено). */
|
/** «Сырое» число строкой (сумма или процент) → число для Number-колонки; '' или мусор → undefined. */
|
||||||
function moneyColumn(raw: string): number | undefined {
|
function numberColumn(raw: string): number | undefined {
|
||||||
const amount = Number(raw);
|
const amount = Number(raw);
|
||||||
return raw.trim() !== '' && Number.isFinite(amount) ? amount : undefined;
|
return raw.trim() !== '' && Number.isFinite(amount) ? amount : undefined;
|
||||||
}
|
}
|
||||||
@@ -62,14 +62,39 @@ function salaryIncreaseColumns(data: IFormData): Record<string, unknown> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
SalaryIncreaseCurrent: moneyColumn(data.SalaryIncreaseCurrent) ?? null,
|
SalaryIncreaseCurrent: numberColumn(data.SalaryIncreaseCurrent) ?? null,
|
||||||
SalaryIncreaseNew: moneyColumn(data.SalaryIncreaseNew) ?? null,
|
SalaryIncreaseNew: numberColumn(data.SalaryIncreaseNew) ?? null,
|
||||||
SalaryIncreaseDate: data.SalaryIncreaseDate || null,
|
SalaryIncreaseDate: data.SalaryIncreaseDate || null,
|
||||||
SalaryIncreaseReason: data.SalaryIncreaseReason || null,
|
SalaryIncreaseReason: data.SalaryIncreaseReason || null,
|
||||||
SalaryIncreaseJustification: data.SalaryIncreaseJustification || null
|
SalaryIncreaseJustification: data.SalaryIncreaseJustification || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Колонки вида «Надбавка / доплата»; вид не выбран — очищаются (см. salaryIncreaseColumns). */
|
||||||
|
function allowanceColumns(data: IFormData): Record<string, unknown> {
|
||||||
|
if (!data.ChangeTypes.includes('allowance')) {
|
||||||
|
return {
|
||||||
|
AllowanceReason: null,
|
||||||
|
AllowanceStartDate: null,
|
||||||
|
AllowanceEndDate: null,
|
||||||
|
AllowanceIndefinite: null,
|
||||||
|
AllowanceAmount: null,
|
||||||
|
AllowanceUnit: null,
|
||||||
|
AllowanceJustification: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
AllowanceReason: data.AllowanceReason || null,
|
||||||
|
AllowanceStartDate: data.AllowanceStartDate || null,
|
||||||
|
// Бессрочно — даты окончания нет, даже если она осталась в состоянии формы.
|
||||||
|
AllowanceEndDate: data.AllowanceIndefinite ? null : data.AllowanceEndDate || null,
|
||||||
|
AllowanceIndefinite: data.AllowanceIndefinite,
|
||||||
|
AllowanceAmount: numberColumn(data.AllowanceAmount) ?? null,
|
||||||
|
AllowanceUnit: data.AllowanceUnit,
|
||||||
|
AllowanceJustification: data.AllowanceJustification || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Поля элемента списка данных из модели формы. */
|
/** Поля элемента списка данных из модели формы. */
|
||||||
function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
@@ -78,7 +103,8 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
|||||||
...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)
|
...salaryIncreaseColumns(data),
|
||||||
|
...allowanceColumns(data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user