feat: feat: брендированная печать, поле «Подразделение» и подача ТМЦ за другого

- Печать доступна только после согласования (Отдел рекрутинга); выбор
  организации и логотип-шапка для форм с несколькими юрлицами
- «СЗ на перемещение ТМЦ»: заявителя можно выбрать (подача за другого),
  маршрут резолвится на выбранного
- «Реферальная рекомендация»: организация вакансии (мультивыбор) и
  подразделение/проект вакансии
- Поле «Подразделение» (справочник) — Vacation, UnpaidLeave, Clearance,
  WorkplaceSetup, RecruitingHire, RecruitingSelection, TmcTransfer
- Поле «Проект» — Vacation, UnpaidLeave, Clearance
This commit is contained in:
Тимур Городецкий
2026-08-26 15:49:25 +05:00
parent f751f03060
commit a4bc54def2
71 changed files with 749 additions and 103 deletions
@@ -35,6 +35,7 @@ import {
IAttachmentInfo
} from '../../shared/attachments/attachments.service';
import { effectiveUserEmail } from '../../shared/impersonation';
import { getEmployeeOrganizations, ICompany, IEmployeeOrganization } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
import { IRequestFormProps } from '../../models/types';
import {
@@ -98,6 +99,57 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editId, editListId]);
// Компании ответственного (Employee/ByEmail.company/companyId) → предзаполнение
// «1.6 Организация» (Companies/CompanyPickerMulti). companyId из Employee/ByEmail
// совпадает с Id из Dictionary/Companies (тот же источник) — доп. запрос не нужен.
// Поле остаётся редактируемым: значения только ПРЕДЛАГАЮТСЯ, как и DepartmentName.
const toCompanies = (records: IEmployeeOrganization[]): ICompany[] =>
records.filter((r) => r.companyId && r.company).map((r) => ({ Id: r.companyId, Name: r.company }));
// Новая заявка — по умолчанию ответственный: сам подающий, подразделение и
// организация — из Employee/ByEmail (Section/company), как в остальных формах
// с авто-«Организацией» (см. shared/dictionary/employeeOrganizations.service.ts).
useEffect(() => {
if (editId) {
return;
}
let alive = true;
const user = context.pageContext.user;
setData((prev) => ({ ...prev, Responsible: prev.Responsible || user.email }));
getEmployeeOrganizations(context, user.email)
.then((records) => {
if (alive) {
setData((prev) => ({
...prev,
DepartmentName: prev.DepartmentName || records[0]?.section || prev.DepartmentName,
Companies: prev.Companies.length > 0 ? prev.Companies : toCompanies(records)
}));
}
})
.catch(() => undefined);
return () => {
alive = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editId]);
// Выбор другого ответственного подтягивает его подразделение и организацию.
const handleResponsibleChange = (login: string): void => {
setData((prev) => ({ ...prev, Responsible: login }));
if (!login) {
return;
}
getEmployeeOrganizations(context, login)
.then((records) =>
setData((prev) => ({
...prev,
DepartmentName: records[0]?.section || prev.DepartmentName,
Companies: toCompanies(records)
}))
)
.catch(() => undefined);
};
// Маршрут согласования резолвится по email инициатора (текущего пользователя).
// У заявки на редактировании маршрут читается из уже записанных строк, чтобы не
// затереть правки инициатора при первом сохранении.
@@ -242,7 +294,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
const sectionProps = { data, set, context };
const sectionProps = { data, set, context, onResponsibleChange: handleResponsibleChange };
return (
<div>
@@ -10,7 +10,8 @@ import {
ApprovalMatrix,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId
useRequestTypeId,
isApprovedStatus
} from '../../shared/approval';
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -118,7 +119,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
return (
<div>
<div className={`${styles.toolbar} rp-no-print`}>
<PrintButton />
{isApprovedStatus(status) && <PrintButton />}
</div>
<div className="rp-print-root">
@@ -25,7 +25,16 @@ export interface IFormData {
ObjectProjectId: string;
ObjectProjectCode: string;
// Отдел
/** Подразделение ОТВЕТСТВЕННОГО за подачу (Employee/ByEmail.Section при выборе Responsible). */
DepartmentName: string;
/**
* Подразделение по справочнику (Dictionary/Departments, см.
* shared/dictionary/DepartmentPicker) — как в WorkplaceSetup: ручной выбор,
* чисто информационное, на маршрут согласования НЕ влияет.
*/
Department: string;
/** Id отдела из справочника; '' — если введён вручную без выбора из списка. */
DepartmentId: string;
OpenVacanciesCount: string;
// Вакансия
Position: string;
@@ -71,6 +80,12 @@ export interface ISectionProps {
data: IFormData;
set: SetField;
context: WebPartContext;
/**
* Выбор ответственного подтягивает его подразделение (Employee/ByEmail →
* Section) — поэтому у поля отдельный обработчик. Если не передан (режим
* чтения) — правка через set.
*/
onResponsibleChange?: (login: string) => void;
}
export const EMPTY_DATA: IFormData = {
@@ -84,6 +99,8 @@ export const EMPTY_DATA: IFormData = {
ObjectProjectId: '',
ObjectProjectCode: '',
DepartmentName: '',
Department: '',
DepartmentId: '',
OpenVacanciesCount: '',
Position: '',
EmploymentType: '',
@@ -13,7 +13,9 @@
{ "internalName": "ObjectProjectCode", "title": "Объект/Проект — код", "type": "Text" },
{ "internalName": "ClosingDeadline", "title": "Крайний срок закрытия", "type": "DateTime", "dateOnly": true },
{ "internalName": "DepartmentName", "title": "Название отдела/департамента", "type": "Text", "required": true },
{ "internalName": "DepartmentName", "title": "Подразделение ответственного за подачу", "type": "Text", "required": true },
{ "internalName": "Department", "title": "Подразделение (справочник)", "type": "Text" },
{ "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" },
{ "internalName": "OpenVacanciesCount", "title": "Количество открытых вакансий", "type": "Number" },
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
@@ -10,7 +10,7 @@ import {
OPEN_REASON_NEW
} from '../model';
export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context }) => {
export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context, onResponsibleChange }) => {
const reason = data.OpenReason;
const showReasonDetails = reason !== '' && reason !== OPEN_REASON_NEW;
const reasonDetailsLabel =
@@ -22,14 +22,22 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
<PeopleField
context={context}
value={data.Responsible}
onChange={(v) => set('Responsible', v)}
onChange={onResponsibleChange ?? ((v) => set('Responsible', v))}
placeholder={PERSON_PLACEHOLDER}
/>
</FormField>
<FormField label="1.2 Дата подачи заявки" htmlFor="SubmitDate">
<FormField label="1.2 Подразделение ответственного за подачу" htmlFor="DepartmentName" required full>
<TextInput
id="DepartmentName"
value={data.DepartmentName}
onChange={(v) => set('DepartmentName', v)}
placeholder="Подставится автоматически по выбранному ответственному"
/>
</FormField>
<FormField label="1.3 Дата подачи заявки" htmlFor="SubmitDate">
<TextInput id="SubmitDate" type="date" value={data.SubmitDate} onChange={(v) => set('SubmitDate', v)} />
</FormField>
<FormField label="1.3 Крайний срок закрытия" htmlFor="ClosingDeadline">
<FormField label="1.4 Крайний срок закрытия" htmlFor="ClosingDeadline">
<TextInput
id="ClosingDeadline"
type="date"
@@ -37,7 +45,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
onChange={(v) => set('ClosingDeadline', v)}
/>
</FormField>
<FormField label="1.4 Причина открытия вакансии" htmlFor="OpenReason">
<FormField label="1.5 Причина открытия вакансии" htmlFor="OpenReason">
<Select
id="OpenReason"
value={data.OpenReason}
@@ -58,7 +66,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
/>
</FormField>
)}
<FormField label="1.5 Организация" htmlFor="Companies" full>
<FormField label="1.6 Организация" htmlFor="Companies" full>
<CompanyPickerMulti
context={context}
id="Companies"
@@ -67,7 +75,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
placeholder="Начните вводить название организации…"
/>
</FormField>
<FormField label="1.6 Объект/Проект" htmlFor="ObjectProject" full>
<FormField label="1.7 Объект/Проект" htmlFor="ObjectProject" full>
<ProjectPicker
context={context}
id="ObjectProject"
@@ -1,11 +1,20 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput } from '../../../shared/ui';
import { DepartmentPicker } from '../../../shared/dictionary';
import { ISectionProps } from '../model';
export const DepartmentSection: React.FC<ISectionProps> = ({ data, set }) => (
export const DepartmentSection: React.FC<ISectionProps> = ({ data, set, context }) => (
<CollapseSection title="2. Отдел / Подразделение" icon="building">
<FormField label="2.1 Название отдела/департамента" htmlFor="DepartmentName" required full>
<TextInput id="DepartmentName" value={data.DepartmentName} onChange={(v) => set('DepartmentName', v)} />
<FormField label="2.1 Подразделение" htmlFor="Department" full>
<DepartmentPicker
id="Department"
context={context}
value={data.Department}
onChange={(name, id) => {
set('Department', name);
set('DepartmentId', id);
}}
/>
</FormField>
<FormField label="2.2 Количество открытых вакансий" htmlFor="OpenVacanciesCount">
<TextInput