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,
|
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). */
|
||||||
|
|||||||
+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',
|
'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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user