feat: инициатор (работник / работодатель) в изменении условий ТД
По ТЗ у видов, которые может начать и сам работник (3, 4, 5, 7), шаблон разный: у работника — только текст заявления, у работодателя — причины из списка и обоснование. Разделяем строго по ТЗ полем «Инициатор»: по умолчанию «на себя — работник, за другого — работодатель» (с подсказкой под полем), следует за сменой сотрудника, пока его не выбрали руками. Причина у работника при сохранении очищается. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,17 +36,11 @@ import {
|
|||||||
dataListTitle,
|
dataListTitle,
|
||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import {
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField, REQUIRED_FIELDS_MESSAGE, routeEmail } from './model';
|
||||||
FORM_ID,
|
|
||||||
IFormData,
|
|
||||||
EMPTY_DATA,
|
|
||||||
SetField,
|
|
||||||
REQUIRED_FIELDS_MESSAGE,
|
|
||||||
routeEmail
|
|
||||||
} from './model';
|
|
||||||
import { saveContractChangeRequest, contractChangeApprovalService } from './submit';
|
import { saveContractChangeRequest, contractChangeApprovalService } from './submit';
|
||||||
import { loadContractChangeRequest } from './load';
|
import { loadContractChangeRequest } from './load';
|
||||||
import { useContractEmployee } from './useContractEmployee';
|
import { useContractEmployee } from './useContractEmployee';
|
||||||
|
import { useInitiatorAutofill } from './useInitiatorAutofill';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
import { ChangeTypeSections } from './byType/ChangeTypeSections';
|
import { ChangeTypeSections } from './byType/ChangeTypeSections';
|
||||||
import { prepareFilesFor } from './byType/attachmentSlots';
|
import { prepareFilesFor } from './byType/attachmentSlots';
|
||||||
@@ -116,10 +110,11 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
|||||||
const employeeControl = useContractEmployee(context, applicantEmail, !editId, data.Employee, (record) =>
|
const employeeControl = useContractEmployee(context, applicantEmail, !editId, data.Employee, (record) =>
|
||||||
set('Employee', record)
|
set('Employee', record)
|
||||||
);
|
);
|
||||||
|
// Пропсы шапки: сотрудник + инициатор, который следует за сотрудником, пока его не выбрали руками.
|
||||||
|
const headerProps = useInitiatorAutofill(!!editId, data, set, applicantEmail, employeeControl);
|
||||||
|
|
||||||
// Маршрут — по сотруднику, без учётной записи — по заявителю (см. routeEmail); при
|
// Маршрут — по сотруднику, без учётной записи — по заявителю (см. routeEmail); при
|
||||||
// переводе в другое подразделение принимающий руководитель — первым шагом.
|
// переводе в другое подразделение принимающий руководитель — первым шагом.
|
||||||
const isDepartmentTransfer = data.ChangeTypes.includes('department-transfer');
|
|
||||||
const { matrix, executors } = useContractRoute({
|
const { matrix, executors } = useContractRoute({
|
||||||
context,
|
context,
|
||||||
approval,
|
approval,
|
||||||
@@ -128,7 +123,8 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
|||||||
editId,
|
editId,
|
||||||
requestStatus,
|
requestStatus,
|
||||||
readOnly,
|
readOnly,
|
||||||
receivingHeadEmail: !readOnly && isDepartmentTransfer ? data.DepartmentTransferReceivingHead : ''
|
receivingHeadEmail:
|
||||||
|
!readOnly && data.ChangeTypes.includes('department-transfer') ? data.DepartmentTransferReceivingHead : ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const isValid = !!data.Employee && data.ChangeTypes.length > 0;
|
const isValid = !!data.Employee && data.ChangeTypes.length > 0;
|
||||||
@@ -257,7 +253,7 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
|||||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||||
|
|
||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<RequestSection data={data} set={set} context={context} employeeControl={employeeControl} />
|
<RequestSection data={data} set={set} context={context} {...headerProps} />
|
||||||
<ChangeTypeSections data={data} set={set} context={context} attachments={attachments} />
|
<ChangeTypeSections data={data} set={set} context={context} attachments={attachments} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,20 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
|||||||
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
|
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
|
||||||
| Остальные 5 | — | ещё не разобраны с кадрами |
|
| Остальные 5 | — | ещё не разобраны с кадрами |
|
||||||
|
|
||||||
Обязательное по виду при отправке (документы, принимающий руководитель, новая
|
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
|
||||||
организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
||||||
|
|
||||||
|
**Инициатор (работник / работодатель)** — решение 2026-10-29: разделяем строго по
|
||||||
|
ТЗ, чтобы у кадров не было вопросов. Поле `ChangeInitiator` в шапке — только у
|
||||||
|
видов, которые по ТЗ может начать и работник (3, 4, 5, 7; `hasInitiatorChoice`).
|
||||||
|
По умолчанию при выборе вида: подаёт на себя — «Работник», за другого —
|
||||||
|
«Работодатель» (`defaultInitiatorRole`), под полем подсказка об этом; можно
|
||||||
|
поменять. Пока инициатора не выбирали руками, он следует за сотрудником: сменили
|
||||||
|
сотрудника — пересчитался (`useInitiatorAutofill`); после ручного выбора и у
|
||||||
|
открытого черновика — не трогается. От роли (`byType/InitiatorReasonFields`): работник — только «Текст
|
||||||
|
заявления» (по ТЗ «От работника: заявление (текст)»), работодатель — причина из
|
||||||
|
списка + «Обоснование». Текст пишется в ту же колонку `*Justification`; причина у
|
||||||
|
работника при сохранении очищается. Остальные поля вида — у обеих ролей.
|
||||||
|
|
||||||
**Принимающий руководитель — временное решение (2026-10-29).** По ТЗ (лист
|
**Принимающий руководитель — временное решение (2026-10-29).** По ТЗ (лист
|
||||||
«Маршрут»: А шаг 2, Б шаг 3 параллельно с непосредственным руководителем) его
|
«Маршрут»: А шаг 2, Б шаг 3 параллельно с непосредственным руководителем) его
|
||||||
|
|||||||
+16
-17
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CollapseSection, FormField, TextInput, TextArea, Select, PeopleField } from '../../../../shared/ui';
|
import { CollapseSection, FormField, TextInput, PeopleField } from '../../../../shared/ui';
|
||||||
|
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
import { DepartmentPicker } from '../../../../shared/dictionary';
|
import { DepartmentPicker } from '../../../../shared/dictionary';
|
||||||
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
@@ -44,21 +45,19 @@ export const DepartmentTransferSection: React.FC<ISectionProps> = ({ data, set,
|
|||||||
placeholder="Начните вводить Ф.И.О. руководителя, который принимает работника…"
|
placeholder="Начните вводить Ф.И.О. руководителя, который принимает работника…"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Причина" htmlFor="DepartmentTransferReason" full>
|
<InitiatorReasonFields
|
||||||
<Select
|
initiator={data.ChangeInitiator}
|
||||||
id="DepartmentTransferReason"
|
reason={{
|
||||||
value={data.DepartmentTransferReason}
|
id: 'DepartmentTransferReason',
|
||||||
onChange={(v) => set('DepartmentTransferReason', v)}
|
value: data.DepartmentTransferReason,
|
||||||
options={DEPARTMENT_TRANSFER_REASONS}
|
options: DEPARTMENT_TRANSFER_REASONS,
|
||||||
placeholder="— выберите —"
|
onChange: (v) => set('DepartmentTransferReason', v)
|
||||||
/>
|
}}
|
||||||
</FormField>
|
justification={{
|
||||||
<FormField label="Обоснование" htmlFor="DepartmentTransferJustification" full>
|
id: 'DepartmentTransferJustification',
|
||||||
<TextArea
|
value: data.DepartmentTransferJustification,
|
||||||
id="DepartmentTransferJustification"
|
onChange: (v) => set('DepartmentTransferJustification', v)
|
||||||
value={data.DepartmentTransferJustification}
|
}}
|
||||||
onChange={(v) => set('DepartmentTransferJustification', v)}
|
/>
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</CollapseSection>
|
</CollapseSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FormField, Select, TextArea, ISelectOption } from '../../../shared/ui';
|
||||||
|
import { InitiatorRole, INITIATOR_EMPLOYEE } from '../changeTypes';
|
||||||
|
import { EMPLOYEE_STATEMENT_LABEL } from '../model';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initiator: InitiatorRole | '';
|
||||||
|
/** Причина из списка — только у работодателя. */
|
||||||
|
reason: { id: string; value: string; options: ISelectOption[]; onChange: (value: string) => void };
|
||||||
|
/** Обоснование работодателя / текст заявления работника — одна и та же колонка вида. */
|
||||||
|
justification: { id: string; value: string; onChange: (value: string) => void };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Окончание секции вида, где по ТЗ шаблон зависит от инициатора:
|
||||||
|
* - работник — «заявление (текст)»: только «Текст заявления», без списка причин;
|
||||||
|
* - работодатель (и пока инициатор не выбран) — «в связи с: а) б) в)»: причина + обоснование.
|
||||||
|
* Поля кладутся прямо в сетку CollapseSection (без обёртки) — как и остальные поля вида.
|
||||||
|
*/
|
||||||
|
export const InitiatorReasonFields: React.FC<Props> = ({ initiator, reason, justification }) => {
|
||||||
|
const isEmployee = initiator === INITIATOR_EMPLOYEE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isEmployee && (
|
||||||
|
<FormField label="Причина" htmlFor={reason.id} full>
|
||||||
|
<Select
|
||||||
|
id={reason.id}
|
||||||
|
value={reason.value}
|
||||||
|
onChange={reason.onChange}
|
||||||
|
options={reason.options}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
<FormField label={isEmployee ? EMPLOYEE_STATEMENT_LABEL : 'Обоснование'} htmlFor={justification.id} full>
|
||||||
|
<TextArea id={justification.id} value={justification.value} onChange={justification.onChange} />
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
-17
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CollapseSection, FormField, TextInput, TextArea, Select } from '../../../../shared/ui';
|
import { CollapseSection, FormField, TextInput } from '../../../../shared/ui';
|
||||||
|
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
import { useFormReadOnly } from '@tnsu/ui-kit';
|
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||||
import { PositionPicker } from '../../../../shared/dictionary';
|
import { PositionPicker } from '../../../../shared/dictionary';
|
||||||
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
||||||
@@ -37,22 +38,20 @@ export const PositionTransferSection: React.FC<ISectionProps> = ({ data, set, co
|
|||||||
onChange={(v) => set('PositionTransferDate', v)}
|
onChange={(v) => set('PositionTransferDate', v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Причина" htmlFor="PositionTransferReason" full>
|
<InitiatorReasonFields
|
||||||
<Select
|
initiator={data.ChangeInitiator}
|
||||||
id="PositionTransferReason"
|
reason={{
|
||||||
value={data.PositionTransferReason}
|
id: 'PositionTransferReason',
|
||||||
onChange={(v) => set('PositionTransferReason', v)}
|
value: data.PositionTransferReason,
|
||||||
options={POSITION_TRANSFER_REASONS}
|
options: POSITION_TRANSFER_REASONS,
|
||||||
placeholder="— выберите —"
|
onChange: (v) => set('PositionTransferReason', v)
|
||||||
/>
|
}}
|
||||||
</FormField>
|
justification={{
|
||||||
<FormField label="Обоснование" htmlFor="PositionTransferJustification" full>
|
id: 'PositionTransferJustification',
|
||||||
<TextArea
|
value: data.PositionTransferJustification,
|
||||||
id="PositionTransferJustification"
|
onChange: (v) => set('PositionTransferJustification', v)
|
||||||
value={data.PositionTransferJustification}
|
}}
|
||||||
onChange={(v) => set('PositionTransferJustification', v)}
|
/>
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
{attachments && (
|
{attachments && (
|
||||||
<AttachmentSlotField
|
<AttachmentSlotField
|
||||||
label={JOB_DESCRIPTION_ATTACHMENT}
|
label={JOB_DESCRIPTION_ATTACHMENT}
|
||||||
|
|||||||
+15
-16
@@ -5,6 +5,7 @@ import { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
|||||||
import { ISectionProps } from '../../model';
|
import { ISectionProps } from '../../model';
|
||||||
import { WORKPLACE_CHANGE_REASONS, WORK_SCHEDULE_OPTIONS, SCHEDULE_OTHER } from './options';
|
import { WORKPLACE_CHANGE_REASONS, WORK_SCHEDULE_OPTIONS, SCHEDULE_OTHER } from './options';
|
||||||
import { useCompanyOptions } from './useCompanyOptions';
|
import { useCompanyOptions } from './useCompanyOptions';
|
||||||
|
import { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||||
import styles from './WorkplaceChangeSection.module.scss';
|
import styles from './WorkplaceChangeSection.module.scss';
|
||||||
|
|
||||||
const PROJECT_PLACEHOLDER = 'Начните вводить название или код проекта…';
|
const PROJECT_PLACEHOLDER = 'Начните вводить название или код проекта…';
|
||||||
@@ -78,15 +79,6 @@ export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, con
|
|||||||
onChange={(v) => set('WorkplaceChangeDate', v)}
|
onChange={(v) => set('WorkplaceChangeDate', v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</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>
|
<FormField label="Условия переезда и компенсации" htmlFor="WorkplaceChangeRelocation" full>
|
||||||
<TextArea
|
<TextArea
|
||||||
id="WorkplaceChangeRelocation"
|
id="WorkplaceChangeRelocation"
|
||||||
@@ -132,13 +124,20 @@ export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, con
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
<FormField label="Обоснование" htmlFor="WorkplaceChangeJustification" full>
|
<InitiatorReasonFields
|
||||||
<TextArea
|
initiator={data.ChangeInitiator}
|
||||||
id="WorkplaceChangeJustification"
|
reason={{
|
||||||
value={data.WorkplaceChangeJustification}
|
id: 'WorkplaceChangeReason',
|
||||||
onChange={(v) => set('WorkplaceChangeJustification', v)}
|
value: data.WorkplaceChangeReason,
|
||||||
/>
|
options: WORKPLACE_CHANGE_REASONS,
|
||||||
</FormField>
|
onChange: (v) => set('WorkplaceChangeReason', v)
|
||||||
|
}}
|
||||||
|
justification={{
|
||||||
|
id: 'WorkplaceChangeJustification',
|
||||||
|
value: data.WorkplaceChangeJustification,
|
||||||
|
onChange: (v) => set('WorkplaceChangeJustification', v)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</CollapseSection>
|
</CollapseSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { IAttachmentEditor } from '../../../shared/attachments/useAttachmentEditor';
|
import { IAttachmentEditor } from '../../../shared/attachments/useAttachmentEditor';
|
||||||
import { IFormData, NEW_COMPANY_REQUIRED_MESSAGE, RECEIVING_HEAD_REQUIRED_MESSAGE } from '../model';
|
import {
|
||||||
|
IFormData,
|
||||||
|
INITIATOR_REQUIRED_MESSAGE,
|
||||||
|
NEW_COMPANY_REQUIRED_MESSAGE,
|
||||||
|
RECEIVING_HEAD_REQUIRED_MESSAGE
|
||||||
|
} from '../model';
|
||||||
|
import { hasInitiatorChoice } from '../changeTypes';
|
||||||
import { missingRequiredAttachments } from './attachmentSlots';
|
import { missingRequiredAttachments } from './attachmentSlots';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,6 +14,8 @@ import { missingRequiredAttachments } from './attachmentSlots';
|
|||||||
* Новое обязательное поле вида = строка здесь.
|
* Новое обязательное поле вида = строка здесь.
|
||||||
*/
|
*/
|
||||||
export function changeTypeSubmitError(data: IFormData, attachments: IAttachmentEditor): string | undefined {
|
export function changeTypeSubmitError(data: IFormData, attachments: IAttachmentEditor): string | undefined {
|
||||||
|
if (hasInitiatorChoice(data.ChangeTypes) && !data.ChangeInitiator) return INITIATOR_REQUIRED_MESSAGE;
|
||||||
|
|
||||||
const missingFiles = missingRequiredAttachments(data, attachments);
|
const missingFiles = missingRequiredAttachments(data, attachments);
|
||||||
if (missingFiles.length > 0) return `Приложите обязательные документы: ${missingFiles.join(', ')}.`;
|
if (missingFiles.length > 0) return `Приложите обязательные документы: ${missingFiles.join(', ')}.`;
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,39 @@ export const CHANGE_TYPE_OPTIONS: ISelectOption[] = (Object.keys(CHANGE_TYPE_LAB
|
|||||||
(type) => ({ value: type, label: CHANGE_TYPE_LABELS[type] })
|
(type) => ({ value: type, label: CHANGE_TYPE_LABELS[type] })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кто инициирует изменение. Значения = choices колонки ChangeInitiator. По ТЗ у
|
||||||
|
* части видов шаблон разный: «От работника: заявление (текст). От работодателя:
|
||||||
|
* прошу… в связи с: а) б) в)» — форма показывает поля той роли, что выбрана.
|
||||||
|
*/
|
||||||
|
export type InitiatorRole = 'Работник' | 'Работодатель';
|
||||||
|
export const INITIATOR_EMPLOYEE: InitiatorRole = 'Работник';
|
||||||
|
export const INITIATOR_EMPLOYER: InitiatorRole = 'Работодатель';
|
||||||
|
export const INITIATOR_OPTIONS: ISelectOption[] = [INITIATOR_EMPLOYEE, INITIATOR_EMPLOYER].map((role) => ({
|
||||||
|
value: role,
|
||||||
|
label: role
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Виды, которые по ТЗ («Кто инициирует») может начать и руководитель, и сам
|
||||||
|
* работник. У остальных инициатор — только руководитель, выбора нет.
|
||||||
|
*/
|
||||||
|
const TYPES_WITH_INITIATOR_CHOICE: ChangeType[] = [
|
||||||
|
'position-transfer',
|
||||||
|
'department-transfer',
|
||||||
|
'workplace-change',
|
||||||
|
'schedule-change'
|
||||||
|
];
|
||||||
|
|
||||||
|
export function hasInitiatorChoice(types: ChangeType[]): boolean {
|
||||||
|
return types.some((type) => TYPES_WITH_INITIATOR_CHOICE.indexOf(type) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Строка из SP → роль; пусто/неизвестное — '' (не выбрана). */
|
||||||
|
export function initiatorRoleFromText(text: string): InitiatorRole | '' {
|
||||||
|
return text === INITIATOR_EMPLOYEE || text === INITIATOR_EMPLOYER ? text : '';
|
||||||
|
}
|
||||||
|
|
||||||
/** Подпись из SP → ключ вида. Неизвестная подпись (переименовали choice) — undefined. */
|
/** Подпись из SP → ключ вида. Неизвестная подпись (переименовали choice) — undefined. */
|
||||||
export function changeTypeFromLabel(label: string): ChangeType | undefined {
|
export function changeTypeFromLabel(label: string): ChangeType | undefined {
|
||||||
const normalized = label.trim();
|
const normalized = label.trim();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getSP } from '../../shared/pnp/pnpjs';
|
|||||||
import { dataListTitle } from '../../shared/listTitles';
|
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, initiatorRoleFromText } from './changeTypes';
|
||||||
import { allowanceUnitFromText } from './byType/Allowance/options';
|
import { allowanceUnitFromText } from './byType/Allowance/options';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
'EmployeePosition',
|
'EmployeePosition',
|
||||||
'EmployeeDepartment',
|
'EmployeeDepartment',
|
||||||
'ChangeTypes',
|
'ChangeTypes',
|
||||||
|
'ChangeInitiator',
|
||||||
'SalaryIncreaseCurrent',
|
'SalaryIncreaseCurrent',
|
||||||
'SalaryIncreaseNew',
|
'SalaryIncreaseNew',
|
||||||
'SalaryIncreaseDate',
|
'SalaryIncreaseDate',
|
||||||
@@ -132,6 +133,7 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
|||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
Employee: employeeFromItem(item),
|
Employee: employeeFromItem(item),
|
||||||
ChangeTypes: changeTypesFromItem(item.ChangeTypes),
|
ChangeTypes: changeTypesFromItem(item.ChangeTypes),
|
||||||
|
ChangeInitiator: initiatorRoleFromText(text(item.ChangeInitiator)),
|
||||||
SalaryIncreaseCurrent: numberText(item.SalaryIncreaseCurrent),
|
SalaryIncreaseCurrent: numberText(item.SalaryIncreaseCurrent),
|
||||||
SalaryIncreaseNew: numberText(item.SalaryIncreaseNew),
|
SalaryIncreaseNew: numberText(item.SalaryIncreaseNew),
|
||||||
// Дату только через spDateOnlyToIso — иначе из-за часового пояса уезжает на день назад.
|
// Дату только через spDateOnlyToIso — иначе из-за часового пояса уезжает на день назад.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
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, InitiatorRole, INITIATOR_EMPLOYEE, INITIATOR_EMPLOYER } from './changeTypes';
|
||||||
import { AllowanceUnit, DEFAULT_ALLOWANCE_UNIT } from './byType/Allowance/options';
|
import { AllowanceUnit, DEFAULT_ALLOWANCE_UNIT } from './byType/Allowance/options';
|
||||||
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||||
|
|
||||||
@@ -33,6 +33,12 @@ export interface IFormData {
|
|||||||
* задел под несколько, см. changeTypes.ts. [] — не выбран.
|
* задел под несколько, см. changeTypes.ts. [] — не выбран.
|
||||||
*/
|
*/
|
||||||
ChangeTypes: ChangeType[];
|
ChangeTypes: ChangeType[];
|
||||||
|
/**
|
||||||
|
* Инициатор — только у видов, где по ТЗ им может быть и работник
|
||||||
|
* (hasInitiatorChoice). От роли зависят поля: у работника — текст заявления,
|
||||||
|
* у работодателя — причины + обоснование. '' — не выбран / у вида выбора нет.
|
||||||
|
*/
|
||||||
|
ChangeInitiator: InitiatorRole | '';
|
||||||
|
|
||||||
// ── Вид «Повышение должностного оклада» (byType/SalaryIncrease). Все необязательные.
|
// ── Вид «Повышение должностного оклада» (byType/SalaryIncrease). Все необязательные.
|
||||||
/** Текущий оклад — «сырые» цифры без пробелов (так отдаёт MoneyInput); '' — не заполнен. */
|
/** Текущий оклад — «сырые» цифры без пробелов (так отдаёт MoneyInput); '' — не заполнен. */
|
||||||
@@ -137,6 +143,7 @@ export interface ISectionProps {
|
|||||||
export const EMPTY_DATA: IFormData = {
|
export const EMPTY_DATA: IFormData = {
|
||||||
Employee: undefined,
|
Employee: undefined,
|
||||||
ChangeTypes: [],
|
ChangeTypes: [],
|
||||||
|
ChangeInitiator: '',
|
||||||
SalaryIncreaseCurrent: '',
|
SalaryIncreaseCurrent: '',
|
||||||
SalaryIncreaseNew: '',
|
SalaryIncreaseNew: '',
|
||||||
SalaryIncreaseDate: '',
|
SalaryIncreaseDate: '',
|
||||||
@@ -188,6 +195,12 @@ 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 INITIATOR_HINT =
|
||||||
|
'Подставляется автоматически: подаёте на себя — «Работник», за другого сотрудника — «Работодатель». Можно изменить.';
|
||||||
|
export const INITIATOR_REQUIRED_MESSAGE = 'Укажите инициатора изменения: работник или работодатель.';
|
||||||
|
/** Подпись поля текста у работника — вместо причин и обоснования (шаблон ТЗ «От работника: заявление»). */
|
||||||
|
export const EMPLOYEE_STATEMENT_LABEL = 'Текст заявления';
|
||||||
export const NEW_COMPANY_REQUIRED_MESSAGE = 'Выберите новую организацию — в какой организации будет новое место работы.';
|
export const NEW_COMPANY_REQUIRED_MESSAGE = 'Выберите новую организацию — в какой организации будет новое место работы.';
|
||||||
export const RECEIVING_HEAD_REQUIRED_MESSAGE =
|
export const RECEIVING_HEAD_REQUIRED_MESSAGE =
|
||||||
'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.';
|
'Укажите принимающего руководителя — его согласие входит в маршрут перевода в другое подразделение.';
|
||||||
@@ -197,6 +210,21 @@ export const RECEIVING_HEAD_REQUIRED_MESSAGE =
|
|||||||
* записи (рабочие на объектах) — заявителя. Берётся от человека, а не от места
|
* записи (рабочие на объектах) — заявителя. Берётся от человека, а не от места
|
||||||
* работы: маршрут от организации не зависит и грузится, пока её ещё выбирают.
|
* работы: маршрут от организации не зависит и грузится, пока её ещё выбирают.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Инициатор по умолчанию (см. INITIATOR_HINT): подаёт на себя — работник, за
|
||||||
|
* другого — работодатель. Пользователь может поменять.
|
||||||
|
*/
|
||||||
|
export function defaultInitiatorRole(
|
||||||
|
person: IEmployeeFullBasePerson | undefined,
|
||||||
|
applicantEmail: string
|
||||||
|
): InitiatorRole {
|
||||||
|
// Сотрудник ещё не выбран/не подгрузился — новая заявка по умолчанию на себя.
|
||||||
|
if (!person) return INITIATOR_EMPLOYEE;
|
||||||
|
// У сотрудника нет учётной записи — это точно не сам заявитель.
|
||||||
|
if (!person.email) return INITIATOR_EMPLOYER;
|
||||||
|
return person.email.toLowerCase() === applicantEmail.toLowerCase() ? INITIATOR_EMPLOYEE : INITIATOR_EMPLOYER;
|
||||||
|
}
|
||||||
|
|
||||||
export function routeEmail(person: IEmployeeFullBasePerson | undefined, applicantEmail: string): string {
|
export function routeEmail(person: IEmployeeFullBasePerson | undefined, applicantEmail: string): string {
|
||||||
if (!person) return '';
|
if (!person) return '';
|
||||||
return person.email || applicantEmail;
|
return person.email || applicantEmail;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)",
|
"Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)",
|
||||||
"Временный перевод при производственной необходимости / простое"
|
"Временный перевод при производственной необходимости / простое"
|
||||||
] },
|
] },
|
||||||
|
{ "internalName": "ChangeInitiator", "title": "Инициатор изменения", "type": "Choice", "choices": ["Работник", "Работодатель"] },
|
||||||
|
|
||||||
{ "internalName": "SalaryIncreaseCurrent", "title": "Повышение оклада: текущий оклад", "type": "Number", "decimals": 0 },
|
{ "internalName": "SalaryIncreaseCurrent", "title": "Повышение оклада: текущий оклад", "type": "Number", "decimals": 0 },
|
||||||
{ "internalName": "SalaryIncreaseNew", "title": "Повышение оклада: новый оклад", "type": "Number", "decimals": 0 },
|
{ "internalName": "SalaryIncreaseNew", "title": "Повышение оклада: новый оклад", "type": "Number", "decimals": 0 },
|
||||||
|
|||||||
@@ -7,13 +7,21 @@ import {
|
|||||||
personFromWorkplace,
|
personFromWorkplace,
|
||||||
workplaceKey
|
workplaceKey
|
||||||
} from '../../../shared/dictionary';
|
} from '../../../shared/dictionary';
|
||||||
import { ChangeType, CHANGE_TYPE_OPTIONS } from '../changeTypes';
|
import {
|
||||||
|
ChangeType,
|
||||||
|
CHANGE_TYPE_OPTIONS,
|
||||||
|
INITIATOR_OPTIONS,
|
||||||
|
InitiatorRole,
|
||||||
|
hasInitiatorChoice,
|
||||||
|
initiatorRoleFromText
|
||||||
|
} from '../changeTypes';
|
||||||
import {
|
import {
|
||||||
ISectionProps,
|
ISectionProps,
|
||||||
EMPLOYEE_PLACEHOLDER,
|
EMPLOYEE_PLACEHOLDER,
|
||||||
WORKPLACE_PLACEHOLDER,
|
WORKPLACE_PLACEHOLDER,
|
||||||
CHANGE_TYPE_PLACEHOLDER,
|
CHANGE_TYPE_PLACEHOLDER,
|
||||||
ROUTE_BY_APPLICANT_NOTE
|
ROUTE_BY_APPLICANT_NOTE,
|
||||||
|
INITIATOR_HINT
|
||||||
} from '../model';
|
} from '../model';
|
||||||
import { IContractEmployee } from '../useContractEmployee';
|
import { IContractEmployee } from '../useContractEmployee';
|
||||||
import styles from './RequestSection.module.scss';
|
import styles from './RequestSection.module.scss';
|
||||||
@@ -21,6 +29,13 @@ import styles from './RequestSection.module.scss';
|
|||||||
interface IRequestSectionProps extends ISectionProps {
|
interface IRequestSectionProps extends ISectionProps {
|
||||||
/** Выбор сотрудника/организации — только на форме подачи; в просмотре не передаётся. */
|
/** Выбор сотрудника/организации — только на форме подачи; в просмотре не передаётся. */
|
||||||
employeeControl?: IContractEmployee;
|
employeeControl?: IContractEmployee;
|
||||||
|
/**
|
||||||
|
* Инициатор по умолчанию при выборе вида с выбором инициатора: подаёт на себя —
|
||||||
|
* работник, за другого — работодатель (см. ContractChangeForm). В просмотре не нужен.
|
||||||
|
*/
|
||||||
|
defaultInitiator?: InitiatorRole;
|
||||||
|
/** Ручной выбор инициатора (см. useInitiatorAutofill); не задан — просто set. */
|
||||||
|
onInitiatorChange?: (role: InitiatorRole | '') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOOP = (): void => undefined;
|
const NOOP = (): void => undefined;
|
||||||
@@ -30,7 +45,14 @@ const NOOP = (): void => undefined;
|
|||||||
* в какой организации (у совместителя их несколько) и что именно меняется.
|
* в какой организации (у совместителя их несколько) и что именно меняется.
|
||||||
* Поля конкретных видов — отдельными секциями (byType/).
|
* Поля конкретных видов — отдельными секциями (byType/).
|
||||||
*/
|
*/
|
||||||
export const RequestSection: React.FC<IRequestSectionProps> = ({ data, set, context, employeeControl }) => {
|
export const RequestSection: React.FC<IRequestSectionProps> = ({
|
||||||
|
data,
|
||||||
|
set,
|
||||||
|
context,
|
||||||
|
employeeControl,
|
||||||
|
defaultInitiator,
|
||||||
|
onInitiatorChange
|
||||||
|
}) => {
|
||||||
const readOnly = useFormReadOnly();
|
const readOnly = useFormReadOnly();
|
||||||
// В просмотре человека нет в состоянии — собираем из сохранённого места работы.
|
// В просмотре человека нет в состоянии — собираем из сохранённого места работы.
|
||||||
const person = employeeControl?.person ?? (data.Employee ? personFromWorkplace(data.Employee) : undefined);
|
const person = employeeControl?.person ?? (data.Employee ? personFromWorkplace(data.Employee) : undefined);
|
||||||
@@ -45,7 +67,12 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({ data, set, cont
|
|||||||
// встанет MultiChoiceField, модель и SP уже готовы.
|
// встанет MultiChoiceField, модель и SP уже готовы.
|
||||||
const selectedType = data.ChangeTypes[0] || '';
|
const selectedType = data.ChangeTypes[0] || '';
|
||||||
const handleTypeChange = (value: string): void => {
|
const handleTypeChange = (value: string): void => {
|
||||||
set('ChangeTypes', value ? [value as ChangeType] : []);
|
const types: ChangeType[] = value ? [value as ChangeType] : [];
|
||||||
|
set('ChangeTypes', types);
|
||||||
|
// Инициатор есть только у видов с выбором: у остальных очищаем, у этих —
|
||||||
|
// подставляем по умолчанию, если ещё не выбран.
|
||||||
|
if (!hasInitiatorChoice(types)) set('ChangeInitiator', '');
|
||||||
|
else if (!data.ChangeInitiator && defaultInitiator) set('ChangeInitiator', defaultInitiator);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,6 +109,22 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({ data, set, cont
|
|||||||
placeholder={CHANGE_TYPE_PLACEHOLDER}
|
placeholder={CHANGE_TYPE_PLACEHOLDER}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
{hasInitiatorChoice(data.ChangeTypes) && (
|
||||||
|
<FormField label="Инициатор" htmlFor="ChangeInitiator" required>
|
||||||
|
<Select
|
||||||
|
id="ChangeInitiator"
|
||||||
|
value={data.ChangeInitiator}
|
||||||
|
onChange={(v) => {
|
||||||
|
const role = initiatorRoleFromText(v);
|
||||||
|
if (onInitiatorChange) onInitiatorChange(role);
|
||||||
|
else set('ChangeInitiator', role);
|
||||||
|
}}
|
||||||
|
options={INITIATOR_OPTIONS}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
{!readOnly && <div className={styles.note}>{INITIATOR_HINT}</div>}
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute }
|
|||||||
import { ISaveItemResult, saveRequestItem } from '../../shared/dataList';
|
import { ISaveItemResult, saveRequestItem } from '../../shared/dataList';
|
||||||
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||||
import { ensureEmployee } from '../../shared/employee';
|
import { ensureEmployee } from '../../shared/employee';
|
||||||
import { CHANGE_TYPE_LABELS } from './changeTypes';
|
import { CHANGE_TYPE_LABELS, INITIATOR_EMPLOYEE, hasInitiatorChoice } from './changeTypes';
|
||||||
import { FORM_ID, IFormData } from './model';
|
import { FORM_ID, IFormData } from './model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,6 +42,16 @@ function employeeColumns(employee: IEmployeeFullBaseRecord | undefined): Record<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Причина из списка — только у работодателя: у работника по ТЗ вместо причин
|
||||||
|
* текст заявления, и списка причин на форме нет. Иначе в заявке осталась бы
|
||||||
|
* причина, выбранная до смены инициатора, которую никто не видит.
|
||||||
|
*/
|
||||||
|
function employerReason(data: IFormData, reason: string): string | undefined {
|
||||||
|
if (data.ChangeInitiator === INITIATOR_EMPLOYEE) return undefined;
|
||||||
|
return reason || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** «Сырое» число строкой (сумма или процент) → число для Number-колонки; '' или мусор → undefined. */
|
/** «Сырое» число строкой (сумма или процент) → число для Number-колонки; '' или мусор → undefined. */
|
||||||
function numberColumn(raw: string): number | undefined {
|
function numberColumn(raw: string): number | undefined {
|
||||||
const amount = Number(raw);
|
const amount = Number(raw);
|
||||||
@@ -109,7 +119,7 @@ function positionTransferColumns(data: IFormData): Record<string, unknown> {
|
|||||||
return {
|
return {
|
||||||
PositionTransferNewPosition: data.PositionTransferNewPosition.trim() || null,
|
PositionTransferNewPosition: data.PositionTransferNewPosition.trim() || null,
|
||||||
PositionTransferDate: data.PositionTransferDate || null,
|
PositionTransferDate: data.PositionTransferDate || null,
|
||||||
PositionTransferReason: data.PositionTransferReason || null,
|
PositionTransferReason: employerReason(data, data.PositionTransferReason) ?? null,
|
||||||
PositionTransferJustification: data.PositionTransferJustification || null
|
PositionTransferJustification: data.PositionTransferJustification || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -133,7 +143,7 @@ function departmentTransferColumns(data: IFormData, receivingHeadId: number | un
|
|||||||
DepartmentTransferNewDepartment: data.DepartmentTransferNewDepartment.trim() || null,
|
DepartmentTransferNewDepartment: data.DepartmentTransferNewDepartment.trim() || null,
|
||||||
DepartmentTransferNewDepartmentId: data.DepartmentTransferNewDepartmentId || null,
|
DepartmentTransferNewDepartmentId: data.DepartmentTransferNewDepartmentId || null,
|
||||||
DepartmentTransferDate: data.DepartmentTransferDate || null,
|
DepartmentTransferDate: data.DepartmentTransferDate || null,
|
||||||
DepartmentTransferReason: data.DepartmentTransferReason || null,
|
DepartmentTransferReason: employerReason(data, data.DepartmentTransferReason) ?? null,
|
||||||
DepartmentTransferJustification: data.DepartmentTransferJustification || null,
|
DepartmentTransferJustification: data.DepartmentTransferJustification || null,
|
||||||
DepartmentTransferReceivingHeadId: receivingHeadId ?? null
|
DepartmentTransferReceivingHeadId: receivingHeadId ?? null
|
||||||
};
|
};
|
||||||
@@ -150,7 +160,7 @@ function workplaceChangeColumns(data: IFormData): Record<string, unknown> {
|
|||||||
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId || null,
|
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId || null,
|
||||||
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim() || null,
|
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim() || null,
|
||||||
WorkplaceChangeDate: data.WorkplaceChangeDate || null,
|
WorkplaceChangeDate: data.WorkplaceChangeDate || null,
|
||||||
WorkplaceChangeReason: data.WorkplaceChangeReason || null,
|
WorkplaceChangeReason: employerReason(data, data.WorkplaceChangeReason) ?? null,
|
||||||
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation || null,
|
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation || null,
|
||||||
WorkplaceChangeRotation: data.WorkplaceChangeRotation,
|
WorkplaceChangeRotation: data.WorkplaceChangeRotation,
|
||||||
// Без вахты графика нет, даже если он остался в состоянии формы.
|
// Без вахты графика нет, даже если он остался в состоянии формы.
|
||||||
@@ -171,6 +181,7 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
|
|||||||
...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]),
|
||||||
|
ChangeInitiator: hasInitiatorChoice(data.ChangeTypes) ? data.ChangeInitiator || null : null,
|
||||||
...salaryIncreaseColumns(data),
|
...salaryIncreaseColumns(data),
|
||||||
...allowanceColumns(data),
|
...allowanceColumns(data),
|
||||||
...positionTransferColumns(data),
|
...positionTransferColumns(data),
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { IEmployeeFullBasePerson } from '../../shared/dictionary';
|
||||||
|
import { InitiatorRole, hasInitiatorChoice } from './changeTypes';
|
||||||
|
import { IFormData, SetField, defaultInitiatorRole } from './model';
|
||||||
|
import { IContractEmployee } from './useContractEmployee';
|
||||||
|
|
||||||
|
/** Пропсы шапки (RequestSection), связанные с сотрудником и инициатором. */
|
||||||
|
export interface IInitiatorAutofill {
|
||||||
|
/** Тот же выбор сотрудника, но со сменой инициатора вслед за сотрудником. */
|
||||||
|
employeeControl: IContractEmployee;
|
||||||
|
/** Инициатор по умолчанию при выборе вида — для текущего сотрудника. */
|
||||||
|
defaultInitiator: InitiatorRole;
|
||||||
|
/** Ручной выбор инициатора — после него автоподстановка больше не вмешивается. */
|
||||||
|
onInitiatorChange: (role: InitiatorRole | '') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициатор следует за сотрудником, пока его не выбрали руками: сменили
|
||||||
|
* сотрудника на себя — «Работник», на другого — «Работодатель» (см.
|
||||||
|
* defaultInitiatorRole). Без этого инициатор, подставленный при выборе вида,
|
||||||
|
* оставался бы прежним после смены сотрудника.
|
||||||
|
*
|
||||||
|
* У открытой заявки (isEdit) инициатор уже сохранён — считаем его выбранным
|
||||||
|
* руками и не перебиваем.
|
||||||
|
*/
|
||||||
|
export function useInitiatorAutofill(
|
||||||
|
isEdit: boolean,
|
||||||
|
data: IFormData,
|
||||||
|
set: SetField,
|
||||||
|
applicantEmail: string,
|
||||||
|
employeeControl: IContractEmployee
|
||||||
|
): IInitiatorAutofill {
|
||||||
|
const isChosenByHandRef = useRef(isEdit);
|
||||||
|
|
||||||
|
const onPersonChange = (person: IEmployeeFullBasePerson | undefined): void => {
|
||||||
|
employeeControl.onPersonChange(person);
|
||||||
|
if (!isChosenByHandRef.current && hasInitiatorChoice(data.ChangeTypes)) {
|
||||||
|
set('ChangeInitiator', defaultInitiatorRole(person, applicantEmail));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInitiatorChange = (role: InitiatorRole | ''): void => {
|
||||||
|
isChosenByHandRef.current = true;
|
||||||
|
set('ChangeInitiator', role);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
employeeControl: { ...employeeControl, onPersonChange },
|
||||||
|
defaultInitiator: defaultInitiatorRole(employeeControl.person, applicantEmail),
|
||||||
|
onInitiatorChange
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user