feat: поля вида «Изменение места работы» в изменении условий ТД
Пятый вид: место работы «было → стало» парой организация + проект (текущая организация — из шапки, новая — из справочника и обязательна), адрес, дата, причина, условия переезда, вахта с графиком работы и обоснование. Проверки «обязательно при отправке» по видам собраны в одну функцию, чтобы форма не росла с каждым новым видом. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,7 +42,6 @@ import {
|
||||
EMPTY_DATA,
|
||||
SetField,
|
||||
REQUIRED_FIELDS_MESSAGE,
|
||||
RECEIVING_HEAD_REQUIRED_MESSAGE,
|
||||
routeEmail
|
||||
} from './model';
|
||||
import { saveContractChangeRequest, contractChangeApprovalService } from './submit';
|
||||
@@ -50,7 +49,8 @@ import { loadContractChangeRequest } from './load';
|
||||
import { useContractEmployee } from './useContractEmployee';
|
||||
import { RequestSection } from './sections/RequestSection';
|
||||
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 { useAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||
import { getAttachments } from '../../shared/attachments/attachments.service';
|
||||
@@ -185,10 +185,9 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
||||
const validateSubmit = (): string | undefined => {
|
||||
const formError = validate();
|
||||
if (formError) return formError;
|
||||
// Обязательные документы — только на отправку: черновик можно сохранить и без файла.
|
||||
const missingFiles = missingRequiredAttachments(data, attachments);
|
||||
if (missingFiles.length > 0) return `Приложите обязательные документы: ${missingFiles.join(', ')}.`;
|
||||
if (isDepartmentTransfer && !data.DepartmentTransferReceivingHead) return RECEIVING_HEAD_REQUIRED_MESSAGE;
|
||||
// Обязательное по виду изменения (документы, принимающий руководитель и т.п.) — только на отправку.
|
||||
const typeError = changeTypeSubmitError(data, attachments);
|
||||
if (typeError) return typeError;
|
||||
if (matrix.error) {
|
||||
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,11 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
||||
| Установление надбавки / доплаты | `Allowance` | основание (а–д: надбавка или доплата видно из пункта), дата начала, дата окончания или «бессрочно» (флажок прячет и очищает дату), размер + переключатель «% / ₸» (`UnitSwitch`, по умолчанию %), обоснование — в т.ч. объём доп. работы при совмещении и текст «иное». Письменное согласие работника на совмещение оформляет кадровик после согласования — не в форме. Вложение **«Результаты аттестации»** — только при основании «г», обязательно |
|
||||
| Перевод на другую должность (постоянный) | `PositionTransfer` | новая должность (`PositionPicker`: справочник `Dictionary/Positions` + ручной ввод), дата перевода, причина (карьерный рост / вакантная должность / новый объект, перераспределение функций / иное), обоснование. Работника и работодателя не различаем — причины видны всем, работник пишет текст заявления в обоснование. Вложение **«Должностная инструкция»** — всегда, обязательно |
|
||||
| Перевод в другое подразделение | `DepartmentTransfer` | новое подразделение (`DepartmentPicker`, имя + id), дата перевода, причина (усиление подразделения / перераспределение функций / вакансия / иное), обоснование, **принимающий руководитель** (People, обязателен при отправке) — см. ниже |
|
||||
| Остальные 6 | — | ещё не разобраны с кадрами |
|
||||
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
|
||||
| Остальные 5 | — | ещё не разобраны с кадрами |
|
||||
|
||||
Обязательное по виду при отправке (документы, принимающий руководитель, новая
|
||||
организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
||||
|
||||
**Принимающий руководитель — временное решение (2026-10-29).** По ТЗ (лист
|
||||
«Маршрут»: А шаг 2, Б шаг 3 параллельно с непосредственным руководителем) его
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SalaryIncreaseSection } from './SalaryIncrease/SalaryIncreaseSection';
|
||||
import { AllowanceSection } from './Allowance/AllowanceSection';
|
||||
import { PositionTransferSection } from './PositionTransfer/PositionTransferSection';
|
||||
import { DepartmentTransferSection } from './DepartmentTransfer/DepartmentTransferSection';
|
||||
import { WorkplaceChangeSection } from './WorkplaceChange/WorkplaceChangeSection';
|
||||
|
||||
/**
|
||||
* Вид изменения → секция с его полями. Partial: поля видов добавляются по мере
|
||||
@@ -15,7 +16,8 @@ const SECTION_BY_TYPE: Partial<Record<ChangeType, React.FC<ISectionProps>>> = {
|
||||
'salary-increase': SalaryIncreaseSection,
|
||||
allowance: AllowanceSection,
|
||||
'position-transfer': PositionTransferSection,
|
||||
'department-transfer': DepartmentTransferSection
|
||||
'department-transfer': DepartmentTransferSection,
|
||||
'workplace-change': WorkplaceChangeSection
|
||||
};
|
||||
|
||||
/** Секции всех выбранных видов, в порядке выбора (сейчас вид один — см. MAX_CHANGE_TYPES). */
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
@import '../../../../../../styles/design-tokens';
|
||||
|
||||
// Ошибка загрузки справочника под полем (не прячем за пустым списком).
|
||||
.fieldError {
|
||||
margin-top: $sp-1;
|
||||
font-size: $fs-label;
|
||||
color: $red-500;
|
||||
}
|
||||
+144
@@ -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 }));
|
||||
+48
@@ -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',
|
||||
'DepartmentTransferReason',
|
||||
'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>;
|
||||
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)),
|
||||
DepartmentTransferReason: text(item.DepartmentTransferReason),
|
||||
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 {
|
||||
|
||||
@@ -92,6 +92,34 @@ export interface IFormData {
|
||||
* маршрута согласования (byType/DepartmentTransfer/receivingHead.ts). '' — не выбран.
|
||||
*/
|
||||
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: '',
|
||||
DepartmentTransferReason: '',
|
||||
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 REQUIRED_FIELDS_MESSAGE = 'Заполните обязательные поля: сотрудник, организация, вид изменения.';
|
||||
export const NEW_COMPANY_REQUIRED_MESSAGE = 'Выберите новую организацию — в какой организации будет новое место работы.';
|
||||
export const RECEIVING_HEAD_REQUIRED_MESSAGE =
|
||||
'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.';
|
||||
|
||||
|
||||
@@ -70,6 +70,25 @@
|
||||
"Иное"
|
||||
] },
|
||||
{ "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> {
|
||||
return {
|
||||
@@ -150,7 +174,8 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
|
||||
...salaryIncreaseColumns(data),
|
||||
...allowanceColumns(data),
|
||||
...positionTransferColumns(data),
|
||||
...departmentTransferColumns(data, receivingHeadId)
|
||||
...departmentTransferColumns(data, receivingHeadId),
|
||||
...workplaceChangeColumns(data)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user