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,
|
||||
dataListTitleForListId
|
||||
} from '../../shared/listTitles';
|
||||
import {
|
||||
FORM_ID,
|
||||
IFormData,
|
||||
EMPTY_DATA,
|
||||
SetField,
|
||||
REQUIRED_FIELDS_MESSAGE,
|
||||
routeEmail
|
||||
} from './model';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, REQUIRED_FIELDS_MESSAGE, routeEmail } from './model';
|
||||
import { saveContractChangeRequest, contractChangeApprovalService } from './submit';
|
||||
import { loadContractChangeRequest } from './load';
|
||||
import { useContractEmployee } from './useContractEmployee';
|
||||
import { useInitiatorAutofill } from './useInitiatorAutofill';
|
||||
import { RequestSection } from './sections/RequestSection';
|
||||
import { ChangeTypeSections } from './byType/ChangeTypeSections';
|
||||
import { prepareFilesFor } from './byType/attachmentSlots';
|
||||
@@ -116,10 +110,11 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
||||
const employeeControl = useContractEmployee(context, applicantEmail, !editId, data.Employee, (record) =>
|
||||
set('Employee', record)
|
||||
);
|
||||
// Пропсы шапки: сотрудник + инициатор, который следует за сотрудником, пока его не выбрали руками.
|
||||
const headerProps = useInitiatorAutofill(!!editId, data, set, applicantEmail, employeeControl);
|
||||
|
||||
// Маршрут — по сотруднику, без учётной записи — по заявителю (см. routeEmail); при
|
||||
// переводе в другое подразделение принимающий руководитель — первым шагом.
|
||||
const isDepartmentTransfer = data.ChangeTypes.includes('department-transfer');
|
||||
const { matrix, executors } = useContractRoute({
|
||||
context,
|
||||
approval,
|
||||
@@ -128,7 +123,8 @@ const ContractChangeForm: React.FC<IRequestFormProps> = ({
|
||||
editId,
|
||||
requestStatus,
|
||||
readOnly,
|
||||
receivingHeadEmail: !readOnly && isDepartmentTransfer ? data.DepartmentTransferReceivingHead : ''
|
||||
receivingHeadEmail:
|
||||
!readOnly && data.ChangeTypes.includes('department-transfer') ? data.DepartmentTransferReceivingHead : ''
|
||||
});
|
||||
|
||||
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>}
|
||||
|
||||
<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} />
|
||||
</FormModeProvider>
|
||||
|
||||
|
||||
@@ -64,8 +64,20 @@ RequestTypeId» в `docs/forms.md`). Тип и маршрут в админке
|
||||
| Изменение места работы (другая местность) | `WorkplaceChange` | место работы «было → стало» парой **организация + проект**: текущая организация — из «Организации» сотрудника (только чтение, отдельно не хранится — это `EmployeeCompany`), текущий проект (`ProjectPicker`); новая организация (`Dictionary/Companies`, **обязательна при отправке**, не подставляется) и новый проект; адрес нового места, дата, причина (новый объект / переезд подразделения или работодателя / иное), условия переезда и компенсации; переключатель **«Вахта»** → «График работы» (тот же набор, что в подборе и приёме, «Другое» → ручной ввод; без вахты график очищается); обоснование |
|
||||
| Остальные 5 | — | ещё не разобраны с кадрами |
|
||||
|
||||
Обязательное по виду при отправке (документы, принимающий руководитель, новая
|
||||
организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
||||
Обязательное по виду при отправке (инициатор, документы, принимающий руководитель,
|
||||
новая организация) — одна функция `byType/submitChecks.ts`; черновик сохраняется без них.
|
||||
|
||||
**Инициатор (работник / работодатель)** — решение 2026-10-29: разделяем строго по
|
||||
ТЗ, чтобы у кадров не было вопросов. Поле `ChangeInitiator` в шапке — только у
|
||||
видов, которые по ТЗ может начать и работник (3, 4, 5, 7; `hasInitiatorChoice`).
|
||||
По умолчанию при выборе вида: подаёт на себя — «Работник», за другого —
|
||||
«Работодатель» (`defaultInitiatorRole`), под полем подсказка об этом; можно
|
||||
поменять. Пока инициатора не выбирали руками, он следует за сотрудником: сменили
|
||||
сотрудника — пересчитался (`useInitiatorAutofill`); после ручного выбора и у
|
||||
открытого черновика — не трогается. От роли (`byType/InitiatorReasonFields`): работник — только «Текст
|
||||
заявления» (по ТЗ «От работника: заявление (текст)»), работодатель — причина из
|
||||
списка + «Обоснование». Текст пишется в ту же колонку `*Justification`; причина у
|
||||
работника при сохранении очищается. Остальные поля вида — у обеих ролей.
|
||||
|
||||
**Принимающий руководитель — временное решение (2026-10-29).** По ТЗ (лист
|
||||
«Маршрут»: А шаг 2, Б шаг 3 параллельно с непосредственным руководителем) его
|
||||
|
||||
+15
-16
@@ -1,5 +1,6 @@
|
||||
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 { CHANGE_TYPE_LABELS } from '../../changeTypes';
|
||||
import { ISectionProps } from '../../model';
|
||||
@@ -44,21 +45,19 @@ export const DepartmentTransferSection: React.FC<ISectionProps> = ({ data, set,
|
||||
placeholder="Начните вводить Ф.И.О. руководителя, который принимает работника…"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Причина" htmlFor="DepartmentTransferReason" full>
|
||||
<Select
|
||||
id="DepartmentTransferReason"
|
||||
value={data.DepartmentTransferReason}
|
||||
onChange={(v) => set('DepartmentTransferReason', v)}
|
||||
options={DEPARTMENT_TRANSFER_REASONS}
|
||||
placeholder="— выберите —"
|
||||
<InitiatorReasonFields
|
||||
initiator={data.ChangeInitiator}
|
||||
reason={{
|
||||
id: 'DepartmentTransferReason',
|
||||
value: data.DepartmentTransferReason,
|
||||
options: DEPARTMENT_TRANSFER_REASONS,
|
||||
onChange: (v) => set('DepartmentTransferReason', v)
|
||||
}}
|
||||
justification={{
|
||||
id: 'DepartmentTransferJustification',
|
||||
value: data.DepartmentTransferJustification,
|
||||
onChange: (v) => set('DepartmentTransferJustification', v)
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Обоснование" htmlFor="DepartmentTransferJustification" full>
|
||||
<TextArea
|
||||
id="DepartmentTransferJustification"
|
||||
value={data.DepartmentTransferJustification}
|
||||
onChange={(v) => set('DepartmentTransferJustification', v)}
|
||||
/>
|
||||
</FormField>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+15
-16
@@ -1,5 +1,6 @@
|
||||
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 { PositionPicker } from '../../../../shared/dictionary';
|
||||
import { AttachmentSlotField } from '../../../../shared/attachments/AttachmentSlotField';
|
||||
@@ -37,22 +38,20 @@ export const PositionTransferSection: React.FC<ISectionProps> = ({ data, set, co
|
||||
onChange={(v) => set('PositionTransferDate', v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Причина" htmlFor="PositionTransferReason" full>
|
||||
<Select
|
||||
id="PositionTransferReason"
|
||||
value={data.PositionTransferReason}
|
||||
onChange={(v) => set('PositionTransferReason', v)}
|
||||
options={POSITION_TRANSFER_REASONS}
|
||||
placeholder="— выберите —"
|
||||
<InitiatorReasonFields
|
||||
initiator={data.ChangeInitiator}
|
||||
reason={{
|
||||
id: 'PositionTransferReason',
|
||||
value: data.PositionTransferReason,
|
||||
options: POSITION_TRANSFER_REASONS,
|
||||
onChange: (v) => set('PositionTransferReason', v)
|
||||
}}
|
||||
justification={{
|
||||
id: 'PositionTransferJustification',
|
||||
value: data.PositionTransferJustification,
|
||||
onChange: (v) => set('PositionTransferJustification', v)
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Обоснование" htmlFor="PositionTransferJustification" full>
|
||||
<TextArea
|
||||
id="PositionTransferJustification"
|
||||
value={data.PositionTransferJustification}
|
||||
onChange={(v) => set('PositionTransferJustification', v)}
|
||||
/>
|
||||
</FormField>
|
||||
{attachments && (
|
||||
<AttachmentSlotField
|
||||
label={JOB_DESCRIPTION_ATTACHMENT}
|
||||
|
||||
+14
-15
@@ -5,6 +5,7 @@ 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 { InitiatorReasonFields } from '../InitiatorReasonFields';
|
||||
import styles from './WorkplaceChangeSection.module.scss';
|
||||
|
||||
const PROJECT_PLACEHOLDER = 'Начните вводить название или код проекта…';
|
||||
@@ -78,15 +79,6 @@ export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, con
|
||||
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"
|
||||
@@ -132,13 +124,20 @@ export const WorkplaceChangeSection: React.FC<ISectionProps> = ({ data, set, con
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField label="Обоснование" htmlFor="WorkplaceChangeJustification" full>
|
||||
<TextArea
|
||||
id="WorkplaceChangeJustification"
|
||||
value={data.WorkplaceChangeJustification}
|
||||
onChange={(v) => set('WorkplaceChangeJustification', v)}
|
||||
<InitiatorReasonFields
|
||||
initiator={data.ChangeInitiator}
|
||||
reason={{
|
||||
id: 'WorkplaceChangeReason',
|
||||
value: data.WorkplaceChangeReason,
|
||||
options: WORKPLACE_CHANGE_REASONS,
|
||||
onChange: (v) => set('WorkplaceChangeReason', v)
|
||||
}}
|
||||
justification={{
|
||||
id: 'WorkplaceChangeJustification',
|
||||
value: data.WorkplaceChangeJustification,
|
||||
onChange: (v) => set('WorkplaceChangeJustification', v)
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -8,6 +14,8 @@ import { missingRequiredAttachments } from './attachmentSlots';
|
||||
* Новое обязательное поле вида = строка здесь.
|
||||
*/
|
||||
export function changeTypeSubmitError(data: IFormData, attachments: IAttachmentEditor): string | undefined {
|
||||
if (hasInitiatorChoice(data.ChangeTypes) && !data.ChangeInitiator) return INITIATOR_REQUIRED_MESSAGE;
|
||||
|
||||
const missingFiles = missingRequiredAttachments(data, attachments);
|
||||
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] })
|
||||
);
|
||||
|
||||
/**
|
||||
* Кто инициирует изменение. Значения = 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. */
|
||||
export function changeTypeFromLabel(label: string): ChangeType | undefined {
|
||||
const normalized = label.trim();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||
import { ChangeType, changeTypeFromLabel } from './changeTypes';
|
||||
import { ChangeType, changeTypeFromLabel, initiatorRoleFromText } from './changeTypes';
|
||||
import { allowanceUnitFromText } from './byType/Allowance/options';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
@@ -88,6 +88,7 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
||||
'EmployeePosition',
|
||||
'EmployeeDepartment',
|
||||
'ChangeTypes',
|
||||
'ChangeInitiator',
|
||||
'SalaryIncreaseCurrent',
|
||||
'SalaryIncreaseNew',
|
||||
'SalaryIncreaseDate',
|
||||
@@ -132,6 +133,7 @@ export async function loadContractChangeRequest(itemId: number, listId?: string)
|
||||
...EMPTY_DATA,
|
||||
Employee: employeeFromItem(item),
|
||||
ChangeTypes: changeTypesFromItem(item.ChangeTypes),
|
||||
ChangeInitiator: initiatorRoleFromText(text(item.ChangeInitiator)),
|
||||
SalaryIncreaseCurrent: numberText(item.SalaryIncreaseCurrent),
|
||||
SalaryIncreaseNew: numberText(item.SalaryIncreaseNew),
|
||||
// Дату только через spDateOnlyToIso — иначе из-за часового пояса уезжает на день назад.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { RequestTypeId } from '../../models/types';
|
||||
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 { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||
|
||||
@@ -33,6 +33,12 @@ export interface IFormData {
|
||||
* задел под несколько, см. changeTypes.ts. [] — не выбран.
|
||||
*/
|
||||
ChangeTypes: ChangeType[];
|
||||
/**
|
||||
* Инициатор — только у видов, где по ТЗ им может быть и работник
|
||||
* (hasInitiatorChoice). От роли зависят поля: у работника — текст заявления,
|
||||
* у работодателя — причины + обоснование. '' — не выбран / у вида выбора нет.
|
||||
*/
|
||||
ChangeInitiator: InitiatorRole | '';
|
||||
|
||||
// ── Вид «Повышение должностного оклада» (byType/SalaryIncrease). Все необязательные.
|
||||
/** Текущий оклад — «сырые» цифры без пробелов (так отдаёт MoneyInput); '' — не заполнен. */
|
||||
@@ -137,6 +143,7 @@ export interface ISectionProps {
|
||||
export const EMPTY_DATA: IFormData = {
|
||||
Employee: undefined,
|
||||
ChangeTypes: [],
|
||||
ChangeInitiator: '',
|
||||
SalaryIncreaseCurrent: '',
|
||||
SalaryIncreaseNew: '',
|
||||
SalaryIncreaseDate: '',
|
||||
@@ -188,6 +195,12 @@ export const CHANGE_TYPE_PLACEHOLDER = '— выберите вид измене
|
||||
export const ROUTE_BY_APPLICANT_NOTE =
|
||||
'У сотрудника нет учётной записи — маршрут согласования построен по заявителю.';
|
||||
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 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 {
|
||||
if (!person) return '';
|
||||
return person.email || applicantEmail;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"Изменение условий труда по инициативе работодателя по причинам экономического, технологического характера, изменений в организации и (или) сокращения объёма работ (снижение оклада, сокращение рабочего времени, изменение графика)",
|
||||
"Временный перевод при производственной необходимости / простое"
|
||||
] },
|
||||
{ "internalName": "ChangeInitiator", "title": "Инициатор изменения", "type": "Choice", "choices": ["Работник", "Работодатель"] },
|
||||
|
||||
{ "internalName": "SalaryIncreaseCurrent", "title": "Повышение оклада: текущий оклад", "type": "Number", "decimals": 0 },
|
||||
{ "internalName": "SalaryIncreaseNew", "title": "Повышение оклада: новый оклад", "type": "Number", "decimals": 0 },
|
||||
|
||||
@@ -7,13 +7,21 @@ import {
|
||||
personFromWorkplace,
|
||||
workplaceKey
|
||||
} from '../../../shared/dictionary';
|
||||
import { ChangeType, CHANGE_TYPE_OPTIONS } from '../changeTypes';
|
||||
import {
|
||||
ChangeType,
|
||||
CHANGE_TYPE_OPTIONS,
|
||||
INITIATOR_OPTIONS,
|
||||
InitiatorRole,
|
||||
hasInitiatorChoice,
|
||||
initiatorRoleFromText
|
||||
} from '../changeTypes';
|
||||
import {
|
||||
ISectionProps,
|
||||
EMPLOYEE_PLACEHOLDER,
|
||||
WORKPLACE_PLACEHOLDER,
|
||||
CHANGE_TYPE_PLACEHOLDER,
|
||||
ROUTE_BY_APPLICANT_NOTE
|
||||
ROUTE_BY_APPLICANT_NOTE,
|
||||
INITIATOR_HINT
|
||||
} from '../model';
|
||||
import { IContractEmployee } from '../useContractEmployee';
|
||||
import styles from './RequestSection.module.scss';
|
||||
@@ -21,6 +29,13 @@ import styles from './RequestSection.module.scss';
|
||||
interface IRequestSectionProps extends ISectionProps {
|
||||
/** Выбор сотрудника/организации — только на форме подачи; в просмотре не передаётся. */
|
||||
employeeControl?: IContractEmployee;
|
||||
/**
|
||||
* Инициатор по умолчанию при выборе вида с выбором инициатора: подаёт на себя —
|
||||
* работник, за другого — работодатель (см. ContractChangeForm). В просмотре не нужен.
|
||||
*/
|
||||
defaultInitiator?: InitiatorRole;
|
||||
/** Ручной выбор инициатора (см. useInitiatorAutofill); не задан — просто set. */
|
||||
onInitiatorChange?: (role: InitiatorRole | '') => void;
|
||||
}
|
||||
|
||||
const NOOP = (): void => undefined;
|
||||
@@ -30,7 +45,14 @@ const NOOP = (): void => undefined;
|
||||
* в какой организации (у совместителя их несколько) и что именно меняется.
|
||||
* Поля конкретных видов — отдельными секциями (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 person = employeeControl?.person ?? (data.Employee ? personFromWorkplace(data.Employee) : undefined);
|
||||
@@ -45,7 +67,12 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({ data, set, cont
|
||||
// встанет MultiChoiceField, модель и SP уже готовы.
|
||||
const selectedType = data.ChangeTypes[0] || '';
|
||||
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 (
|
||||
@@ -82,6 +109,22 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({ data, set, cont
|
||||
placeholder={CHANGE_TYPE_PLACEHOLDER}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute }
|
||||
import { ISaveItemResult, saveRequestItem } from '../../shared/dataList';
|
||||
import { IEmployeeFullBaseRecord } from '../../shared/dictionary';
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -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. */
|
||||
function numberColumn(raw: string): number | undefined {
|
||||
const amount = Number(raw);
|
||||
@@ -109,7 +119,7 @@ function positionTransferColumns(data: IFormData): Record<string, unknown> {
|
||||
return {
|
||||
PositionTransferNewPosition: data.PositionTransferNewPosition.trim() || null,
|
||||
PositionTransferDate: data.PositionTransferDate || null,
|
||||
PositionTransferReason: data.PositionTransferReason || null,
|
||||
PositionTransferReason: employerReason(data, data.PositionTransferReason) ?? null,
|
||||
PositionTransferJustification: data.PositionTransferJustification || null
|
||||
};
|
||||
}
|
||||
@@ -133,7 +143,7 @@ function departmentTransferColumns(data: IFormData, receivingHeadId: number | un
|
||||
DepartmentTransferNewDepartment: data.DepartmentTransferNewDepartment.trim() || null,
|
||||
DepartmentTransferNewDepartmentId: data.DepartmentTransferNewDepartmentId || null,
|
||||
DepartmentTransferDate: data.DepartmentTransferDate || null,
|
||||
DepartmentTransferReason: data.DepartmentTransferReason || null,
|
||||
DepartmentTransferReason: employerReason(data, data.DepartmentTransferReason) ?? null,
|
||||
DepartmentTransferJustification: data.DepartmentTransferJustification || null,
|
||||
DepartmentTransferReceivingHeadId: receivingHeadId ?? null
|
||||
};
|
||||
@@ -150,7 +160,7 @@ function workplaceChangeColumns(data: IFormData): Record<string, unknown> {
|
||||
WorkplaceChangeNewProjectId: data.WorkplaceChangeNewProjectId || null,
|
||||
WorkplaceChangeAddress: data.WorkplaceChangeAddress.trim() || null,
|
||||
WorkplaceChangeDate: data.WorkplaceChangeDate || null,
|
||||
WorkplaceChangeReason: data.WorkplaceChangeReason || null,
|
||||
WorkplaceChangeReason: employerReason(data, data.WorkplaceChangeReason) ?? null,
|
||||
WorkplaceChangeRelocation: data.WorkplaceChangeRelocation || null,
|
||||
WorkplaceChangeRotation: data.WorkplaceChangeRotation,
|
||||
// Без вахты графика нет, даже если он остался в состоянии формы.
|
||||
@@ -171,6 +181,7 @@ function buildItem(data: IFormData, status: string, receivingHeadId: number | un
|
||||
...employeeColumns(data.Employee),
|
||||
// MultiChoice пишется плоским массивом подписей (как «База 1С» в Ticket1C); [] очищает колонку.
|
||||
ChangeTypes: data.ChangeTypes.map((type) => CHANGE_TYPE_LABELS[type]),
|
||||
ChangeInitiator: hasInitiatorChoice(data.ChangeTypes) ? data.ChangeInitiator || null : null,
|
||||
...salaryIncreaseColumns(data),
|
||||
...allowanceColumns(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