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
@@ -9,6 +9,9 @@
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: $sp-3;
margin-bottom: $sp-4;
}
@@ -2,7 +2,7 @@ import * as React from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';
import type { WebPartContext } from '@microsoft/sp-webpart-base';
import styles from './UnpaidLeaveView.module.scss';
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintButton } from '../../shared/ui';
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintButton, PrintOrgSelect, OrgLetterhead } from '../../shared/ui';
import {
ApproverService,
DecisionPanel,
@@ -45,6 +45,9 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
const [code, setCode] = useState<string>('');
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Какая из организаций сотрудника (см. OrganizationField) идёт в печать —
// выбор в тулбаре через PrintOrgSelect, по умолчанию первая.
const [printOrgIndex, setPrintOrgIndex] = useState<number>(0);
// listId уже проверен в ApproveFeature (не смонтировались бы сюда иначе) —
// unpaidLeaveApprovalService можно звать не опасаясь исключения.
@@ -98,7 +101,7 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
);
}
const sectionProps = { data, set: NOOP_SET, context };
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
const notify = (): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
@@ -115,10 +118,12 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
return (
<div>
<div className={`${styles.toolbar} rp-no-print`}>
<PrintOrgSelect value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
<PrintButton />
</div>
<div className="rp-print-root">
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
<FormCard
title="Заявка на отпуск без содержания"
subtitle={`№${code || itemId}`}
@@ -30,6 +30,10 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
'EndDate',
'Reason',
'Organization',
'Department',
'DepartmentId',
'Project',
'ProjectId',
'RequestStatus',
'RequestUniqueCode',
'Employee/Title',
@@ -51,7 +55,11 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
EndDate: end.slice(0, 10),
Reason: (item.Reason as string) || '',
Substitute: substitute?.EMail || '',
Organization: (item.Organization as string) || ''
Organization: (item.Organization as string) || '',
Department: (item.Department as string) || '',
DepartmentId: (item.DepartmentId as string) || '',
Project: (item.Project as string) || '',
ProjectId: (item.ProjectId as string) || ''
};
return {
@@ -33,6 +33,18 @@ export interface IFormData {
* read-only (см. shared/ui/OrganizationField).
*/
Organization: string;
/**
* Подразделение (справочник Dictionary/Departments, см.
* shared/dictionary/DepartmentPicker) — необязательное, справочное, на
* маршрут согласования НЕ влияет.
*/
Department: string;
/** Id отдела из справочника; '' — если введён вручную без выбора из списка. */
DepartmentId: string;
/** Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — необязательное, справочное. */
Project: string;
/** oid проекта ('' — если введён вручную без выбора из списка). */
ProjectId: string;
}
/** Обновление одного поля формы с сохранением типа значения. */
@@ -48,6 +60,8 @@ export interface ISectionProps {
* отдельный обработчик. Если не передан (режим чтения) — правка через set.
*/
onEmployeeChange?: (login: string) => void;
/** Индекс организации для печати (см. shared/ui/PrintOrgSelect и OrganizationField.printIndex). */
printOrgIndex?: number;
}
export const EMPTY_DATA: IFormData = {
@@ -57,7 +71,11 @@ export const EMPTY_DATA: IFormData = {
EndDate: '',
Reason: '',
Substitute: '',
Organization: ''
Organization: '',
Department: '',
DepartmentId: '',
Project: '',
ProjectId: ''
};
/** Плейсхолдер для поля выбора сотрудника. */
@@ -11,6 +11,10 @@
{ "internalName": "DaysCount", "title": "Количество дней", "type": "Number" },
{ "internalName": "Reason", "title": "Основание (в связи с)", "type": "Note" },
{ "internalName": "Substitute", "title": "Лицо, замещающее на время отсутствия", "type": "User", "userSelectionMode": "PeopleOnly" },
{ "internalName": "Organization", "title": "Организация", "type": "Note" }
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
{ "internalName": "Department", "title": "Подразделение", "type": "Text" },
{ "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" },
{ "internalName": "Project", "title": "Проект", "type": "Text" },
{ "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" }
]
}
@@ -1,10 +1,11 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea, PeopleField, OrganizationField } from '../../../shared/ui';
import { DepartmentPicker, ProjectPicker } from '../../../shared/dictionary';
import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER, MIN_NOTICE_DAYS } from '../model';
import { isoDatePlusDays } from '../../../shared/dateUtils';
/** Единственная секция формы «Отпуск без содержания»: сотрудник, должность, период, основание, замещающий. */
export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange }) => (
export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange, printOrgIndex }) => (
<CollapseSection title="Заявка на отпуск без содержания" icon="people">
<FormField label="Сотрудник" htmlFor="Employee" required full>
<PeopleField
@@ -17,7 +18,30 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, on
<FormField label="Должность" htmlFor="Position" required>
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
</FormField>
<OrganizationField value={data.Organization} />
<OrganizationField value={data.Organization} printIndex={printOrgIndex} />
<FormField label="Подразделение" htmlFor="Department">
<DepartmentPicker
id="Department"
context={context}
value={data.Department}
onChange={(name, id) => {
set('Department', name);
set('DepartmentId', id);
}}
/>
</FormField>
<FormField label="Проект" htmlFor="Project">
<ProjectPicker
id="Project"
context={context}
value={data.Project}
onChange={(name, id) => {
set('Project', name);
set('ProjectId', id);
}}
placeholder="Начните вводить название или код проекта…"
/>
</FormField>
<FormField label="Дата начала" htmlFor="StartDate" required>
<TextInput
id="StartDate"
@@ -49,7 +49,11 @@ function buildItem(
DaysCount: days ?? null,
Reason: data.Reason || null,
SubstituteId: substituteId ?? null,
Organization: data.Organization || null
Organization: data.Organization || null,
Department: data.Department || null,
DepartmentId: data.DepartmentId || null,
Project: data.Project || null,
ProjectId: data.ProjectId || null
};
}