feat: поля вида «Изменение места работы» в изменении условий ТД

Пятый вид: место работы «было → стало» парой организация + проект
(текущая организация — из шапки, новая — из справочника и обязательна),
адрес, дата, причина, условия переезда, вахта с графиком работы и
обоснование. Проверки «обязательно при отправке» по видам собраны в
одну функцию, чтобы форма не росла с каждым новым видом.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-29 14:33:25 +05:00
parent 87c9f92d48
commit c4b2cb3c3d
12 changed files with 387 additions and 13 deletions
@@ -42,7 +42,6 @@ import {
EMPTY_DATA, EMPTY_DATA,
SetField, SetField,
REQUIRED_FIELDS_MESSAGE, REQUIRED_FIELDS_MESSAGE,
RECEIVING_HEAD_REQUIRED_MESSAGE,
routeEmail routeEmail
} from './model'; } from './model';
import { saveContractChangeRequest, contractChangeApprovalService } from './submit'; import { saveContractChangeRequest, contractChangeApprovalService } from './submit';
@@ -50,7 +49,8 @@ import { loadContractChangeRequest } from './load';
import { useContractEmployee } from './useContractEmployee'; import { useContractEmployee } from './useContractEmployee';
import { RequestSection } from './sections/RequestSection'; import { RequestSection } from './sections/RequestSection';
import { ChangeTypeSections } from './byType/ChangeTypeSections'; import { ChangeTypeSections } from './byType/ChangeTypeSections';
import { missingRequiredAttachments, prepareFilesFor } from './byType/attachmentSlots'; import { prepareFilesFor } from './byType/attachmentSlots';
import { changeTypeSubmitError } from './byType/submitChecks';
import { useContractRoute, matrixToApprovers } from './useContractRoute'; import { useContractRoute, matrixToApprovers } from './useContractRoute';
import { useAttachmentEditor } from '../../shared/attachments/useAttachmentEditor'; import { useAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
import { getAttachments } from '../../shared/attachments/attachments.service'; import { getAttachments } from '../../shared/attachments/attachments.service';
@@ -185,10 +185,9 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
const validateSubmit = (): string | undefined => { const validateSubmit = (): string | undefined => {
const formError = validate(); const formError = validate();
if (formError) return formError; if (formError) return formError;
// Обязательные документы — только на отправку: черновик можно сохранить и без файла. // Обязательное по виду изменения (документы, принимающий руководитель и т.п.) — только на отправку.
const missingFiles = missingRequiredAttachments(data, attachments); const typeError = changeTypeSubmitError(data, attachments);
if (missingFiles.length > 0) return `Приложите обязательные документы: ${missingFiles.join(', ')}.`; if (typeError) return typeError;
if (isDepartmentTransfer && !data.DepartmentTransferReceivingHead) return RECEIVING_HEAD_REQUIRED_MESSAGE;
if (matrix.error) { if (matrix.error) {
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
} }
@@ -61,7 +61,11 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
| Установление надбавки / доплаты | `Allowance` | основание (а–д: надбавка или доплата видно из пункта), дата начала, дата окончания или «бессрочно» (флажок прячет и очищает дату), размер + переключатель «% / ₸» (`UnitSwitch`, по умолчанию %), обоснование — в т.ч. объём доп. работы при совмещении и текст «иное». Письменное согласие работника на совмещение оформляет кадровик после согласования — не в форме. Вложение **«Результаты аттестации»** — только при основании «г», обязательно | | Установление надбавки / доплаты | `Allowance` | основание (а–д: надбавка или доплата видно из пункта), дата начала, дата окончания или «бессрочно» (флажок прячет и очищает дату), размер + переключатель «% / ₸» (`UnitSwitch`, по умолчанию %), обоснование — в т.ч. объём доп. работы при совмещении и текст «иное». Письменное согласие работника на совмещение оформляет кадровик после согласования — не в форме. Вложение **«Результаты аттестации»** — только при основании «г», обязательно |
| Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно | | Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно |
| Перевод в другое подразделение | `DepartmentTransfer` | новое подразделение (`DepartmentPicker`, имя + id), дата перевода, причина (усиление подразделения / перераспределение функций / вакансия / иное), обоснование, **принимающий руководитель** (People, обязателен при отправке) — см. ниже | | Перевод в другое подразделение | `DepartmentTransfer` | новое подразделение (`DepartmentPicker`, имя + id), дата перевода, причина (усиление подразделения / перераспределение функций / вакансия / иное), обоснование, **принимающий руководитель** (People, обязателен при отправке) — см. ниже |
| Остальные 6 | — | ещё не разобраны с кадрами | | Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
| Остальные 5 | — | ещё не разобраны с кадрами |
Обязательное по виду при отправке (документы, принимающий руководитель, новая
организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
**Принимающий руководитель — временное решение (2026-10-29).** По ТЗ (лист **Принимающий руководитель — временное решение (2026-10-29).** По ТЗ (лист
«Маршрут»: А шаг 2, Б шаг 3 параллельно с непосредственным руководителем) его «Маршрут»: А шаг 2, Б шаг 3 параллельно с непосредственным руководителем) его
@@ -5,6 +5,7 @@ import { SalaryIncreaseSection } from './SalaryIncrease/SalaryIncreaseSection';
import { AllowanceSection } from './Allowance/AllowanceSection'; import { AllowanceSection } from './Allowance/AllowanceSection';
import { PositionTransferSection } from './PositionTransfer/PositionTransferSection'; import { PositionTransferSection } from './PositionTransfer/PositionTransferSection';
import { DepartmentTransferSection } from './DepartmentTransfer/DepartmentTransferSection'; import { DepartmentTransferSection } from './DepartmentTransfer/DepartmentTransferSection';
import { WorkplaceChangeSection } from './WorkplaceChange/WorkplaceChangeSection';
/** /**
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере * Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
@@ -15,7 +16,8 @@ const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
'salary-increase': SalaryIncreaseSection, 'salary-increase': SalaryIncreaseSection,
allowance: AllowanceSection, allowance: AllowanceSection,
'position-transfer': PositionTransferSection, 'position-transfer': PositionTransferSection,
'department-transfer': DepartmentTransferSection 'department-transfer': DepartmentTransferSection,
'workplace-change': WorkplaceChangeSection
}; };
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */ /** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
@@ -0,0 +1,8 @@
@import '../../../../../../styles/design-tokens';
// Ошибка загрузки справочника под полем (не прячем за пустым списком).
.fieldError {
margin-top: $sp-1;
font-size: $fs-label;
color: $red-500;
}
@@ -0,0 +1,144 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea, Select, Toggle } from '../../../../shared/ui';
import { ProjectPicker } from '../../../../shared/dictionary';
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
import { ISectionProps } from '../../model';
import { WORKPLACE_CHANGE_REASONS, WORK_SCHEDULE_OPTIONS, SCHEDULE_OTHER } from './options';
import { useCompanyOptions } from './useCompanyOptions';
import styles from './WorkplaceChangeSection.module.scss';
const PROJECT_PLACEHOLDER = 'Начните вводить название или код проекта…';
/**
* Поля вида «Изменение места работы (другая местность)». Состав согласован с
* Тимуром по ТЗ кадров: место работы «было → стало» парой «организация + проект»
* (текущая организация — из выбранной в шапке, новая обязательна), адрес, дата,
* причина (а–в), условия переезда, вахта с графиком работы, обоснование.
*
* Поля — прямо в CollapseSection, без FormGrid (тело секции само сетка).
*/
export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, context }) => {
const companies = useCompanyOptions(context);
return (
<CollapseSection title={CHANGE_TYPE_LABELS['workplace-change']} icon="vehicle">
<FormField label="Текущая организация" htmlFor="WorkplaceChangeCurrentCompany">
{/* Та же, что выбрана в «Организации» сотрудника, — второй раз не вводим. */}
<TextInput id="WorkplaceChangeCurrentCompany" value={data.Employee?.company || ''} onChange={() => undefined} readOnly />
</FormField>
<FormField label="Текущий проект" htmlFor="WorkplaceChangeCurrentProject">
<ProjectPicker
context={context}
id="WorkplaceChangeCurrentProject"
value={data.WorkplaceChangeCurrentProject}
onChange={(name, id) => {
set('WorkplaceChangeCurrentProject', name);
set('WorkplaceChangeCurrentProjectId', id);
}}
placeholder={PROJECT_PLACEHOLDER}
/>
</FormField>
<FormField label="Новая организация" htmlFor="WorkplaceChangeNewCompanyId" required>
<Select
id="WorkplaceChangeNewCompanyId"
value={data.WorkplaceChangeNewCompanyId}
onChange={(id) => {
set('WorkplaceChangeNewCompanyId', id);
set('WorkplaceChangeNewCompany', companies.byId.get(id)?.Name || '');
}}
options={companies.options}
placeholder={companies.loading ? 'Загрузка…' : '— выберите —'}
/>
{companies.error && <div className={styles.fieldError}>{companies.error}</div>}
</FormField>
<FormField label="Новый проект" htmlFor="WorkplaceChangeNewProject">
<ProjectPicker
context={context}
id="WorkplaceChangeNewProject"
value={data.WorkplaceChangeNewProject}
onChange={(name, id) => {
set('WorkplaceChangeNewProject', name);
set('WorkplaceChangeNewProjectId', id);
}}
placeholder={PROJECT_PLACEHOLDER}
/>
</FormField>
<FormField label="Адрес нового места работы" htmlFor="WorkplaceChangeAddress" full>
<TextInput
id="WorkplaceChangeAddress"
value={data.WorkplaceChangeAddress}
onChange={(v) => set('WorkplaceChangeAddress', v)}
/>
</FormField>
<FormField label="Дата изменения" htmlFor="WorkplaceChangeDate">
<TextInput
id="WorkplaceChangeDate"
type="date"
value={data.WorkplaceChangeDate}
onChange={(v) => set('WorkplaceChangeDate', v)}
/>
</FormField>
<FormField label="Причина" htmlFor="WorkplaceChangeReason">
<Select
id="WorkplaceChangeReason"
value={data.WorkplaceChangeReason}
onChange={(v) => set('WorkplaceChangeReason', v)}
options={WORKPLACE_CHANGE_REASONS}
placeholder="— выберите —"
/>
</FormField>
<FormField label="Условия переезда и компенсации" htmlFor="WorkplaceChangeRelocation" full>
<TextArea
id="WorkplaceChangeRelocation"
value={data.WorkplaceChangeRelocation}
onChange={(v) => set('WorkplaceChangeRelocation', v)}
/>
</FormField>
<FormField label="Вахта" htmlFor="WorkplaceChangeRotation" full>
<Toggle
id="WorkplaceChangeRotation"
checked={data.WorkplaceChangeRotation}
onChange={(checked) => {
set('WorkplaceChangeRotation', checked);
// Без вахты график не нужен — очищаем, чтобы не сохранить скрытое значение.
if (!checked) {
set('WorkplaceChangeSchedule', '');
set('WorkplaceChangeScheduleOther', '');
}
}}
onLabel="Да"
/>
</FormField>
{data.WorkplaceChangeRotation && (
<FormField label="График работы" htmlFor="WorkplaceChangeSchedule">
<Select
id="WorkplaceChangeSchedule"
value={data.WorkplaceChangeSchedule}
onChange={(v) => {
set('WorkplaceChangeSchedule', v);
if (v !== SCHEDULE_OTHER) set('WorkplaceChangeScheduleOther', '');
}}
options={WORK_SCHEDULE_OPTIONS}
placeholder="— выберите —"
/>
</FormField>
)}
{data.WorkplaceChangeRotation && data.WorkplaceChangeSchedule === SCHEDULE_OTHER && (
<FormField label="График работы (другое)" htmlFor="WorkplaceChangeScheduleOther">
<TextInput
id="WorkplaceChangeScheduleOther"
value={data.WorkplaceChangeScheduleOther}
onChange={(v) => set('WorkplaceChangeScheduleOther', v)}
/>
</FormField>
)}
<FormField label="Обоснование" htmlFor="WorkplaceChangeJustification" full>
<TextArea
id="WorkplaceChangeJustification"
value={data.WorkplaceChangeJustification}
onChange={(v) => set('WorkplaceChangeJustification', v)}
/>
</FormField>
</CollapseSection>
);
};
@@ -0,0 +1,33 @@
import { ISelectOption } from '../../../../shared/ui';
/**
* Причины изменения места работы — по шаблону текста ТЗ кадров (а–в).
* Работника и работодателя не различаем (как в видах 3–4).
* Значения совпадают с choices колонки WorkplaceChangeReason (provisioning.json).
*/
export const WORKPLACE_CHANGE_REASONS: ISelectOption[] = [
'Открытие нового объекта',
'Переезд подразделения / работодателя в другую местность',
'Иное'
].map((reason) => ({ value: reason, label: reason }));
/** «Другое» в графике работы — открывает ручной ввод. */
export const SCHEDULE_OTHER = 'Другое';
/**
* Варианты «График работы» при вахте — тот же набор, что в «Заявке на подбор» и
* «Заявке на приём» (WORK_SCHEDULE_OPTIONS там): формы независимы, значения
* продублированы намеренно, как и между ними.
*/
export const WORK_SCHEDULE_OPTIONS: ISelectOption[] = [
'5/2',
'Вахтовый 30/30',
'Вахтовый 20/10',
'Вахтовый 30/15',
'Вахтовый 22/8',
'Вахтовый 15/15',
'6/1',
'1/2',
'2/2',
SCHEDULE_OTHER
].map((schedule) => ({ value: schedule, label: schedule }));
@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { ISelectOption } from '../../../../shared/ui';
import { ICompany, getCompanies } from '../../../../shared/dictionary';
export interface ICompanyOptions {
options: ISelectOption[];
/** Компании по Id — чтобы по выбранному значению Select получить название. */
byId: Map<string, ICompany>;
loading: boolean;
error: string;
}
/**
* Справочник компаний (GET /Dictionary/Companies) как варианты Select: value — Id,
* label — название. Список кешируется в самом сервисе (localStorage, 4 часа).
* Не загрузился — ошибка видна под полем, не прячем её за пустым списком.
*/
export function useCompanyOptions(context: WebPartContext): ICompanyOptions {
const [companies, setCompanies] = useState<ICompany[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>('');
useEffect(() => {
let alive = true;
getCompanies(context)
.then((list) => {
if (!alive) return;
setCompanies(list);
setLoading(false);
})
.catch(() => {
if (!alive) return;
setError('Не удалось загрузить список организаций. Обновите страницу.');
setLoading(false);
});
return () => {
alive = false;
};
}, [context]);
return {
options: companies.map((c) => ({ value: c.Id, label: c.Name })),
byId: new Map(companies.map((c) => [c.Id, c])),
loading,
error
};
}
@@ -0,0 +1,21 @@
import { IAttachmentEditor } from '../../../shared/attachments/useAttachmentEditor';
import { IFormData, NEW_COMPANY_REQUIRED_MESSAGE, RECEIVING_HEAD_REQUIRED_MESSAGE } from '../model';
import { missingRequiredAttachments } from './attachmentSlots';
/**
* Что обязательно по выбранному виду изменения — проверяется только при ОТПРАВКЕ:
* черновик сохраняется и без этого. Первая найденная проблема — текстом; ничего — undefined.
* Новое обязательное поле вида = строка здесь.
*/
export function changeTypeSubmitError(data: IFormData, attachments: IAttachmentEditor): string | undefined {
const missingFiles = missingRequiredAttachments(data, attachments);
if (missingFiles.length > 0) return `Приложите обязательные документы: ${missingFiles.join(', ')}.`;
if (data.ChangeTypes.includes('department-transfer') && !data.DepartmentTransferReceivingHead) {
return RECEIVING_HEAD_REQUIRED_MESSAGE;
}
if (data.ChangeTypes.includes('workplace-change') && !data.WorkplaceChangeNewCompanyId) {
return NEW_COMPANY_REQUIRED_MESSAGE;
}
return undefined;
}
@@ -109,7 +109,21 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
'DepartmentTransferDate', 'DepartmentTransferDate',
'DepartmentTransferReason', 'DepartmentTransferReason',
'DepartmentTransferJustification', 'DepartmentTransferJustification',
'DepartmentTransferReceivingHead/EMail' 'DepartmentTransferReceivingHead/EMail',
'WorkplaceChangeCurrentProject',
'WorkplaceChangeCurrentProjectId',
'WorkplaceChangeNewCompany',
'WorkplaceChangeNewCompanyId',
'WorkplaceChangeNewProject',
'WorkplaceChangeNewProjectId',
'WorkplaceChangeAddress',
'WorkplaceChangeDate',
'WorkplaceChangeReason',
'WorkplaceChangeRelocation',
'WorkplaceChangeRotation',
'WorkplaceChangeSchedule',
'WorkplaceChangeScheduleOther',
'WorkplaceChangeJustification'
) )
.expand('DepartmentTransferReceivingHead')()) as Record<string, unknown>; .expand('DepartmentTransferReceivingHead')()) as Record<string, unknown>;
const receivingHead = item.DepartmentTransferReceivingHead as { EMail?: string } | undefined; const receivingHead = item.DepartmentTransferReceivingHead as { EMail?: string } | undefined;
@@ -140,7 +154,21 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
DepartmentTransferDate: spDateOnlyToIso(text(item.DepartmentTransferDate)), DepartmentTransferDate: spDateOnlyToIso(text(item.DepartmentTransferDate)),
DepartmentTransferReason: text(item.DepartmentTransferReason), DepartmentTransferReason: text(item.DepartmentTransferReason),
DepartmentTransferJustification: text(item.DepartmentTransferJustification), DepartmentTransferJustification: text(item.DepartmentTransferJustification),
DepartmentTransferReceivingHead: receivingHead?.EMail || '' DepartmentTransferReceivingHead: receivingHead?.EMail || '',
WorkplaceChangeCurrentProject: text(item.WorkplaceChangeCurrentProject),
WorkplaceChangeCurrentProjectId: text(item.WorkplaceChangeCurrentProjectId),
WorkplaceChangeNewCompany: text(item.WorkplaceChangeNewCompany),
WorkplaceChangeNewCompanyId: text(item.WorkplaceChangeNewCompanyId),
WorkplaceChangeNewProject: text(item.WorkplaceChangeNewProject),
WorkplaceChangeNewProjectId: text(item.WorkplaceChangeNewProjectId),
WorkplaceChangeAddress: text(item.WorkplaceChangeAddress),
WorkplaceChangeDate: spDateOnlyToIso(text(item.WorkplaceChangeDate)),
WorkplaceChangeReason: text(item.WorkplaceChangeReason),
WorkplaceChangeRelocation: text(item.WorkplaceChangeRelocation),
WorkplaceChangeRotation: item.WorkplaceChangeRotation === true,
WorkplaceChangeSchedule: text(item.WorkplaceChangeSchedule),
WorkplaceChangeScheduleOther: text(item.WorkplaceChangeScheduleOther),
WorkplaceChangeJustification: text(item.WorkplaceChangeJustification)
}; };
return { return {
@@ -92,6 +92,34 @@ export interface IFormData {
* маршрута согласования (byType/DepartmentTransfer/receivingHead.ts). '' — не выбран. * маршрута согласования (byType/DepartmentTransfer/receivingHead.ts). '' — не выбран.
*/ */
DepartmentTransferReceivingHead: string; DepartmentTransferReceivingHead: string;
// ── Вид «Изменение места работы (другая местность)» (byType/WorkplaceChange).
// Место работы = организация + проект. Текущая организация не хранится отдельно —
// это EmployeeCompany (выбранная в шапке). Обязательна при отправке только новая организация.
/** Текущий проект (справочник /Project, ProjectPicker) и его oid; '' — не выбран / введён вручную. */
WorkplaceChangeCurrentProject: string;
WorkplaceChangeCurrentProjectId: string;
/** Новая организация (справочник Dictionary/Companies): название и Id. */
WorkplaceChangeNewCompany: string;
WorkplaceChangeNewCompanyId: string;
/** Новый проект и его oid. */
WorkplaceChangeNewProject: string;
WorkplaceChangeNewProjectId: string;
/** Адрес нового места работы. */
WorkplaceChangeAddress: string;
/** С какой даты, ISO YYYY-MM-DD. */
WorkplaceChangeDate: string;
/** Причина — одно из WORKPLACE_CHANGE_REASONS (Choice-колонка); '' — не выбрана. */
WorkplaceChangeReason: string;
/** Условия переезда и компенсации (если переезд). */
WorkplaceChangeRelocation: string;
/** Вахта — тогда показывается график работы. */
WorkplaceChangeRotation: boolean;
/** График работы — одно из WORK_SCHEDULE_OPTIONS; при «Другое» текст — в WorkplaceChangeScheduleOther. */
WorkplaceChangeSchedule: string;
WorkplaceChangeScheduleOther: string;
/** Обоснование; при «Иное» — его текст, у работника — текст заявления. */
WorkplaceChangeJustification: string;
} }
/** Обновление одного поля формы с сохранением типа значения. */ /** Обновление одного поля формы с сохранением типа значения. */
@@ -130,7 +158,21 @@ export const EMPTY_DATA: IFormData = {
DepartmentTransferDate: '', DepartmentTransferDate: '',
DepartmentTransferReason: '', DepartmentTransferReason: '',
DepartmentTransferJustification: '', DepartmentTransferJustification: '',
DepartmentTransferReceivingHead: '' DepartmentTransferReceivingHead: '',
WorkplaceChangeCurrentProject: '',
WorkplaceChangeCurrentProjectId: '',
WorkplaceChangeNewCompany: '',
WorkplaceChangeNewCompanyId: '',
WorkplaceChangeNewProject: '',
WorkplaceChangeNewProjectId: '',
WorkplaceChangeAddress: '',
WorkplaceChangeDate: '',
WorkplaceChangeReason: '',
WorkplaceChangeRelocation: '',
WorkplaceChangeRotation: false,
WorkplaceChangeSchedule: '',
WorkplaceChangeScheduleOther: '',
WorkplaceChangeJustification: ''
}; };
/** Плейсхолдер для поля выбора сотрудника. */ /** Плейсхолдер для поля выбора сотрудника. */
@@ -146,6 +188,7 @@ export const CHANGE_TYPE_PLACEHOLDER = '— выберите вид измене
export const ROUTE_BY_APPLICANT_NOTE = export const ROUTE_BY_APPLICANT_NOTE =
'У сотрудника нет учётной записи — маршрут согласования построен по заявителю.'; 'У сотрудника нет учётной записи — маршрут согласования построен по заявителю.';
export const REQUIRED_FIELDS_MESSAGE = 'Заполните обязательные поля: сотрудник, организация, вид изменения.'; export const REQUIRED_FIELDS_MESSAGE = 'Заполните обязательные поля: сотрудник, организация, вид изменения.';
export const NEW_COMPANY_REQUIRED_MESSAGE = 'Выберите новую организацию — в какой организации будет новое место работы.';
export const RECEIVING_HEAD_REQUIRED_MESSAGE = export const RECEIVING_HEAD_REQUIRED_MESSAGE =
'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.'; 'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.';
@@ -70,6 +70,25 @@
"Иное" "Иное"
] }, ] },
{ "internalName": "DepartmentTransferJustification", "title": "Перевод в подразделение: обоснование", "type": "Note" }, { "internalName": "DepartmentTransferJustification", "title": "Перевод в подразделение: обоснование", "type": "Note" },
{ "internalName": "DepartmentTransferReceivingHead", "title": "Перевод в подразделение: принимающий руководитель", "type": "User", "userSelectionMode": "PeopleOnly" } { "internalName": "DepartmentTransferReceivingHead", "title": "Перевод в подразделение: принимающий руководитель", "type": "User", "userSelectionMode": "PeopleOnly" },
{ "internalName": "WorkplaceChangeCurrentProject", "title": "Место работы: текущий проект", "type": "Text" },
{ "internalName": "WorkplaceChangeCurrentProjectId", "title": "Место работы: id текущего проекта", "type": "Text" },
{ "internalName": "WorkplaceChangeNewCompany", "title": "Место работы: новая организация", "type": "Text" },
{ "internalName": "WorkplaceChangeNewCompanyId", "title": "Место работы: id новой организации", "type": "Text" },
{ "internalName": "WorkplaceChangeNewProject", "title": "Место работы: новый проект", "type": "Text" },
{ "internalName": "WorkplaceChangeNewProjectId", "title": "Место работы: id нового проекта", "type": "Text" },
{ "internalName": "WorkplaceChangeAddress", "title": "Место работы: адрес нового места", "type": "Note" },
{ "internalName": "WorkplaceChangeDate", "title": "Место работы: дата изменения", "type": "DateTime", "dateOnly": true },
{ "internalName": "WorkplaceChangeReason", "title": "Место работы: причина", "type": "Choice", "choices": [
"Открытие нового объекта",
"Переезд подразделения / работодателя в другую местность",
"Иное"
] },
{ "internalName": "WorkplaceChangeRelocation", "title": "Место работы: условия переезда и компенсации", "type": "Note" },
{ "internalName": "WorkplaceChangeRotation", "title": "Место работы: вахта", "type": "Boolean" },
{ "internalName": "WorkplaceChangeSchedule", "title": "Место работы: график работы", "type": "Text" },
{ "internalName": "WorkplaceChangeScheduleOther", "title": "Место работы: график работы (другое)", "type": "Text" },
{ "internalName": "WorkplaceChangeJustification", "title": "Место работы: обоснование", "type": "Note" }
] ]
} }
@@ -139,6 +139,30 @@ function departmentTransferColumns(data: IFormData, receivingHeadId: number | un
}; };
} }
/** Колонки вида «Изменение места работы»; вид не выбран — очищаются. */
function workplaceChangeColumns(data: IFormData): Record<string, unknown> {
const columns = {
WorkplaceChangeCurrentProject: data.WorkplaceChangeCurrentProject.trim() || null,
WorkplaceChangeCurrentProjectId: data.WorkplaceChangeCurrentProjectId || null,
WorkplaceChangeNewCompany: data.WorkplaceChangeNewCompany || null,
WorkplaceChangeNewCompanyId: data.WorkplaceChangeNewCompanyId || null,
WorkplaceChangeNewProject: data.WorkplaceChangeNewProject.trim() || null,
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId || null,
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim() || null,
WorkplaceChangeDate: data.WorkplaceChangeDate || null,
WorkplaceChangeReason: data.WorkplaceChangeReason || null,
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation || null,
WorkplaceChangeRotation: data.WorkplaceChangeRotation,
// Без вахты графика нет, даже если он остался в состоянии формы.
WorkplaceChangeSchedule: (data.WorkplaceChangeRotation && data.WorkplaceChangeSchedule) || null,
WorkplaceChangeScheduleOther: (data.WorkplaceChangeRotation && data.WorkplaceChangeScheduleOther.trim()) || null,
WorkplaceChangeJustification: data.WorkplaceChangeJustification || null
};
if (data.ChangeTypes.includes('workplace-change')) return columns;
// Тот же набор ключей, все — null: колонки невыбранного вида очищаются.
return Object.keys(columns).reduce<Record<string, unknown>>((acc, key) => ({ ...acc, [key]: null }), {});
}
/** Поля элемента списка данных из модели формы. */ /** Поля элемента списка данных из модели формы. */
function buildItem(data: IFormData, status: string, receivingHeadId: number | undefined): Record<string, unknown> { function buildItem(data: IFormData, status: string, receivingHeadId: number | undefined): Record<string, unknown> {
return { return {
@@ -150,7 +174,8 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
...salaryIncreaseColumns(data), ...salaryIncreaseColumns(data),
...allowanceColumns(data), ...allowanceColumns(data),
...positionTransferColumns(data), ...positionTransferColumns(data),
...departmentTransferColumns(data, receivingHeadId) ...departmentTransferColumns(data, receivingHeadId),
...workplaceChangeColumns(data)
}; };
} }