feat: feat: брендированная печать, поле «Подразделение» и подача ТМЦ за другого
- Печать доступна только после согласования (Отдел рекрутинга); выбор организации и логотип-шапка для форм с несколькими юрлицами - «СЗ на перемещение ТМЦ»: заявителя можно выбрать (подача за другого), маршрут резолвится на выбранного - «Реферальная рекомендация»: организация вакансии (мультивыбор) и подразделение/проект вакансии - Поле «Подразделение» (справочник) — Vacation, UnpaidLeave, Clearance, WorkplaceSetup, RecruitingHire, RecruitingSelection, TmcTransfer - Поле «Проект» — Vacation, UnpaidLeave, Clearance
This commit is contained in:
@@ -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 './ReferralView.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,
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
useMatrixEditor,
|
||||
notifyActiveStep,
|
||||
useRequestTypeId,
|
||||
APPROVAL_PHASE
|
||||
APPROVAL_PHASE,
|
||||
isApprovedStatus
|
||||
} from '../../shared/approval';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import {
|
||||
@@ -50,6 +51,9 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
||||
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 (не смонтировались бы сюда иначе) —
|
||||
// referralApprovalService можно звать не опасаясь исключения.
|
||||
@@ -121,7 +125,7 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
||||
);
|
||||
}
|
||||
|
||||
const sectionProps = { data, set: NOOP_SET, context };
|
||||
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
|
||||
|
||||
const notify = (): void => {
|
||||
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
|
||||
@@ -138,10 +142,16 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
||||
return (
|
||||
<div>
|
||||
<div className={`${styles.toolbar} rp-no-print`}>
|
||||
<PrintButton />
|
||||
{isApprovedStatus(status) && (
|
||||
<>
|
||||
<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}`}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
/** Безопасный разбор JSON-массива из Note-колонки (пустой при ошибке/пустом). */
|
||||
function parseJsonArray<T>(raw: unknown): T[] {
|
||||
if (typeof raw !== 'string' || raw.trim() === '') return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? (parsed as T[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export interface ILoadedRequest {
|
||||
data: IFormData;
|
||||
/** Ф.И.О. рекомендателя из поля Referrer (для заголовка режима чтения). */
|
||||
@@ -35,7 +47,9 @@ export async function loadReferralRequest(itemId: number, listId?: string): Prom
|
||||
'CandidateEmail',
|
||||
'CandidateCity',
|
||||
'VacancyTitle',
|
||||
'VacancyDepartment',
|
||||
'VacancyProject',
|
||||
'VacancyProjectId',
|
||||
'VacancyCompaniesJson',
|
||||
'CurrentEmployer',
|
||||
'CurrentPosition',
|
||||
'ExperienceYears',
|
||||
@@ -69,7 +83,9 @@ export async function loadReferralRequest(itemId: number, listId?: string): Prom
|
||||
CandidateEmail: (item.CandidateEmail as string) || '',
|
||||
CandidateCity: (item.CandidateCity as string) || '',
|
||||
VacancyTitle: (item.VacancyTitle as string) || '',
|
||||
VacancyDepartment: (item.VacancyDepartment as string) || '',
|
||||
VacancyProject: (item.VacancyProject as string) || '',
|
||||
VacancyProjectId: (item.VacancyProjectId as string) || '',
|
||||
VacancyCompanies: parseJsonArray<ICompany>(item.VacancyCompaniesJson),
|
||||
CurrentEmployer: (item.CurrentEmployer as string) || '',
|
||||
CurrentPosition: (item.CurrentPosition as string) || '',
|
||||
ExperienceYears: (item.ExperienceYears as string) || '',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { RequestTypeId } from '../../models/types';
|
||||
import { ISelectOption } from '../../shared/ui';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
|
||||
/**
|
||||
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||
@@ -55,7 +56,18 @@ export interface IFormData {
|
||||
CandidateCity: string;
|
||||
|
||||
VacancyTitle: string;
|
||||
VacancyDepartment: string;
|
||||
/** Название проекта (справочник /Project, см. shared/dictionary/ProjectPicker). */
|
||||
VacancyProject: string;
|
||||
/** oid проекта ('' — если введён вручную без выбора из списка). */
|
||||
VacancyProjectId: string;
|
||||
/**
|
||||
* Организация(и), в которую рекомендуют кандидата (справочник, мультивыбор,
|
||||
* см. shared/dictionary/CompanyPickerMulti) — хранится VacancyCompaniesJson.
|
||||
* Необязательное, чисто информативное поле: на маршрут согласования НЕ влияет
|
||||
* (в отличие от Companies в «Заявке на прием»). Не путать с Organization выше —
|
||||
* то поле про самого рекомендателя, это — про вакансию.
|
||||
*/
|
||||
VacancyCompanies: ICompany[];
|
||||
|
||||
CurrentEmployer: string;
|
||||
CurrentPosition: string;
|
||||
@@ -85,6 +97,8 @@ export interface ISectionProps {
|
||||
* обработчик. Если не передан (режим чтения) — правка через set.
|
||||
*/
|
||||
onReferrerChange?: (login: string) => void;
|
||||
/** Индекс организации для печати (см. shared/ui/PrintOrgSelect и OrganizationField.printIndex). */
|
||||
printOrgIndex?: number;
|
||||
}
|
||||
|
||||
export const EMPTY_DATA: IFormData = {
|
||||
@@ -99,7 +113,9 @@ export const EMPTY_DATA: IFormData = {
|
||||
CandidateEmail: '',
|
||||
CandidateCity: '',
|
||||
VacancyTitle: '',
|
||||
VacancyDepartment: '',
|
||||
VacancyProject: '',
|
||||
VacancyProjectId: '',
|
||||
VacancyCompanies: [],
|
||||
CurrentEmployer: '',
|
||||
CurrentPosition: '',
|
||||
ExperienceYears: '',
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
{ "internalName": "CandidateCity", "title": "Город проживания кандидата", "type": "Text" },
|
||||
|
||||
{ "internalName": "VacancyTitle", "title": "Название вакансии", "type": "Text", "required": true },
|
||||
{ "internalName": "VacancyDepartment", "title": "Подразделение / Объект вакансии", "type": "Text" },
|
||||
{ "internalName": "VacancyCompaniesJson", "title": "Организация вакансии (JSON)", "type": "Note" },
|
||||
{ "internalName": "VacancyProject", "title": "Подразделение / Проект вакансии", "type": "Text" },
|
||||
{ "internalName": "VacancyProjectId", "title": "Id проекта вакансии", "type": "Text" },
|
||||
|
||||
{ "internalName": "CurrentEmployer", "title": "Текущее место работы кандидата", "type": "Text" },
|
||||
{ "internalName": "CurrentPosition", "title": "Текущая должность кандидата", "type": "Text" },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CollapseSection, FormField, TextInput, PhoneInput, PeopleField, Organiz
|
||||
import { ISectionProps, REFERRER_PLACEHOLDER } from '../model';
|
||||
|
||||
/** Секция 1: кто рекомендует — дата подачи, сам рекомендатель и его контакты. */
|
||||
export const ReferrerSection: React.FC<ISectionProps> = ({ data, set, context, onReferrerChange }) => (
|
||||
export const ReferrerSection: React.FC<ISectionProps> = ({ data, set, context, onReferrerChange, printOrgIndex }) => (
|
||||
<CollapseSection title="Рекомендатель" icon="people">
|
||||
<FormField label="Дата подачи" htmlFor="SubmitDate" required>
|
||||
<TextInput id="SubmitDate" type="date" value={data.SubmitDate} onChange={(v) => set('SubmitDate', v)} />
|
||||
@@ -31,7 +31,7 @@ export const ReferrerSection: React.FC<ISectionProps> = ({ data, set, context, o
|
||||
placeholder="Подставится автоматически по выбранному сотруднику"
|
||||
/>
|
||||
</FormField>
|
||||
<OrganizationField value={data.Organization} />
|
||||
<OrganizationField value={data.Organization} printIndex={printOrgIndex} />
|
||||
<FormField label="Контактный телефон" htmlFor="ReferrerPhone" required>
|
||||
<PhoneInput id="ReferrerPhone" value={data.ReferrerPhone} onChange={(v) => set('ReferrerPhone', v)} />
|
||||
</FormField>
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextInput } from '../../../shared/ui';
|
||||
import { CompanyPickerMulti, ProjectPicker } from '../../../shared/dictionary';
|
||||
import { ISectionProps } from '../model';
|
||||
|
||||
/** Секция 3: на какую вакансию рекомендуют. */
|
||||
export const VacancySection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
export const VacancySection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||
<CollapseSection title="На какую вакансию рекомендуете" icon="search">
|
||||
<FormField label="Название вакансии" htmlFor="VacancyTitle" required full>
|
||||
<TextInput id="VacancyTitle" value={data.VacancyTitle} onChange={(v) => set('VacancyTitle', v)} />
|
||||
</FormField>
|
||||
<FormField label="Подразделение / Объект" htmlFor="VacancyDepartment" full>
|
||||
<TextInput
|
||||
id="VacancyDepartment"
|
||||
value={data.VacancyDepartment}
|
||||
onChange={(v) => set('VacancyDepartment', v)}
|
||||
<FormField label="Организация" htmlFor="VacancyCompanies" full>
|
||||
<CompanyPickerMulti
|
||||
id="VacancyCompanies"
|
||||
context={context}
|
||||
value={data.VacancyCompanies}
|
||||
onChange={(companies) => set('VacancyCompanies', companies)}
|
||||
placeholder="Начните вводить название организации…"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Подразделение / Проект" htmlFor="VacancyProject" full>
|
||||
<ProjectPicker
|
||||
id="VacancyProject"
|
||||
context={context}
|
||||
value={data.VacancyProject}
|
||||
onChange={(name, id) => {
|
||||
set('VacancyProject', name);
|
||||
set('VacancyProjectId', id);
|
||||
}}
|
||||
placeholder="Начните вводить название или код проекта…"
|
||||
/>
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
|
||||
@@ -39,7 +39,10 @@ function buildItem(data: IFormData, status: string, referrerId: number): Record<
|
||||
CandidateEmail: data.CandidateEmail || null,
|
||||
CandidateCity: data.CandidateCity || null,
|
||||
VacancyTitle: data.VacancyTitle || null,
|
||||
VacancyDepartment: data.VacancyDepartment || null,
|
||||
VacancyProject: data.VacancyProject || null,
|
||||
VacancyProjectId: data.VacancyProjectId || null,
|
||||
// Мультивыбор компаний не разворачивается в колонки — сериализуем в Note (см. Companies в RecruitingHire).
|
||||
VacancyCompaniesJson: data.VacancyCompanies.length > 0 ? JSON.stringify(data.VacancyCompanies) : null,
|
||||
CurrentEmployer: data.CurrentEmployer || null,
|
||||
CurrentPosition: data.CurrentPosition || null,
|
||||
ExperienceYears: data.ExperienceYears || null,
|
||||
|
||||
Reference in New Issue
Block a user