Compare commits
2 Commits
df29d080ed
...
418e30dfc3
| Author | SHA1 | Date | |
|---|---|---|---|
| 418e30dfc3 | |||
| a4bc54def2 |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -9,7 +9,10 @@
|
||||
|
||||
.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 './ClearanceView.module.scss';
|
||||
import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, PrintButton } from '../../shared/ui';
|
||||
import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, PrintButton, PrintOrgSelect, OrgLetterhead } from '../../shared/ui';
|
||||
import {
|
||||
ApproverService,
|
||||
DecisionPanel,
|
||||
@@ -62,6 +62,9 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
||||
// ввода (набрал сумму → data стал непустым → поле сразу стало бы readOnly).
|
||||
const [lockedWriteOffs, setLockedWriteOffs] = useState<Set<string>>(new Set());
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||
// Какая из организаций сотрудника (см. OrganizationField) идёт в печать —
|
||||
// выбор в тулбаре через PrintOrgSelect, по умолчанию первая.
|
||||
const [printOrgIndex, setPrintOrgIndex] = useState<number>(0);
|
||||
const [saveErrorMessage, setSaveErrorMessage] = useState<string>('');
|
||||
|
||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||
@@ -240,7 +243,8 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
||||
context,
|
||||
hrRecordsEditable,
|
||||
parkingEditable,
|
||||
writeOffEditableKeys: editableWriteOffKeys
|
||||
writeOffEditableKeys: editableWriteOffKeys,
|
||||
printOrgIndex
|
||||
};
|
||||
|
||||
const notify = (): void => {
|
||||
@@ -258,6 +262,7 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
||||
return (
|
||||
<div>
|
||||
<div className={`${styles.toolbar} rp-no-print`}>
|
||||
<PrintOrgSelect value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
||||
<PrintButton />
|
||||
</div>
|
||||
|
||||
@@ -277,6 +282,7 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
||||
)}
|
||||
|
||||
<div className="rp-print-root">
|
||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||
<FormCard
|
||||
title="Обходной лист"
|
||||
subtitle={`№${code || itemId}`}
|
||||
|
||||
@@ -37,6 +37,8 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
|
||||
'AccessCard',
|
||||
'TrainingAmount',
|
||||
'Organization',
|
||||
'Project',
|
||||
'ProjectId',
|
||||
'DismissalUrl',
|
||||
'WriteOffAssetsAmount',
|
||||
'WriteOffAssetsComment',
|
||||
@@ -71,6 +73,8 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
|
||||
AccessCard: !!item.AccessCard,
|
||||
TrainingAmount: amount === null || amount === undefined ? '' : String(amount),
|
||||
Organization: (item.Organization as string) || '',
|
||||
Project: (item.Project as string) || '',
|
||||
ProjectId: (item.ProjectId as string) || '',
|
||||
// Гиперссылка приходит объектом {Url, Description} — модели нужен только адрес.
|
||||
DismissalUrl: (item.DismissalUrl as { Url?: string } | undefined)?.Url || '',
|
||||
WriteOffAssetsAmount: numberField(item.WriteOffAssetsAmount),
|
||||
|
||||
@@ -31,6 +31,10 @@ export interface IFormData {
|
||||
* read-only (см. shared/ui/OrganizationField).
|
||||
*/
|
||||
Organization: string;
|
||||
/** Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — необязательное, справочное. */
|
||||
Project: string;
|
||||
/** oid проекта ('' — если введён вручную без выбора из списка). */
|
||||
ProjectId: string;
|
||||
/**
|
||||
* Ссылка на заявление на увольнение, по которому создан этот лист — колонка-
|
||||
* гиперссылка `DismissalUrl`. Форма её не пишет (заполняет автосоздание, см.
|
||||
@@ -122,6 +126,8 @@ export interface ISectionProps {
|
||||
* отдельный обработчик. Если не передан (режим чтения) — правка через set.
|
||||
*/
|
||||
onEmployeeChange?: (login: string) => void;
|
||||
/** Индекс организации для печати (см. shared/ui/PrintOrgSelect и OrganizationField.printIndex). */
|
||||
printOrgIndex?: number;
|
||||
}
|
||||
|
||||
export const EMPTY_DATA: IFormData = {
|
||||
@@ -133,6 +139,8 @@ export const EMPTY_DATA: IFormData = {
|
||||
AccessCard: false,
|
||||
TrainingAmount: '',
|
||||
Organization: '',
|
||||
Project: '',
|
||||
ProjectId: '',
|
||||
DismissalUrl: '',
|
||||
WriteOffAssetsAmount: '',
|
||||
WriteOffAssetsComment: '',
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
{ "internalName": "AccessCard", "title": "Карта СКУД", "type": "Boolean" },
|
||||
{ "internalName": "TrainingAmount", "title": "Обучение: сумма", "type": "Number", "decimals": 2 },
|
||||
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
|
||||
{ "internalName": "Project", "title": "Проект", "type": "Text" },
|
||||
{ "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" },
|
||||
|
||||
{ "internalName": "DismissalUrl", "title": "Ссылка на заявление на увольнение", "type": "Url" },
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextArea, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField, LinkField } from '../../../shared/ui';
|
||||
import { ProjectPicker } from '../../../shared/dictionary';
|
||||
import { ISectionProps, EMPLOYEE_PLACEHOLDER, WRITE_OFF_CATEGORIES } from '../model';
|
||||
|
||||
// Подразделение подтягивается автоматически при выборе сотрудника (см.
|
||||
@@ -30,6 +31,7 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
||||
onEmployeeChange,
|
||||
hrRecordsEditable,
|
||||
parkingEditable,
|
||||
printOrgIndex,
|
||||
writeOffEditableKeys
|
||||
}) => (
|
||||
<CollapseSection title="Обходной лист" icon="people">
|
||||
@@ -63,7 +65,19 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
||||
placeholder="Подставится автоматически по выбранному сотруднику"
|
||||
/>
|
||||
</FormField>
|
||||
<OrganizationField value={data.Organization} />
|
||||
<OrganizationField value={data.Organization} printIndex={printOrgIndex} />
|
||||
<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="TerminationDate" required>
|
||||
<TextInput
|
||||
id="TerminationDate"
|
||||
|
||||
@@ -101,7 +101,9 @@ function buildItem(data: IFormData, status: string, employeeId: number, employee
|
||||
ParkingCard: data.ParkingCard,
|
||||
AccessCard: data.AccessCard,
|
||||
TrainingAmount: Number.isFinite(amount) && data.TrainingAmount.trim() !== '' ? amount : null,
|
||||
Organization: data.Organization || null
|
||||
Organization: data.Organization || null,
|
||||
Project: data.Project || null,
|
||||
ProjectId: data.ProjectId || null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
useMatrixEditor,
|
||||
notifyActiveStep,
|
||||
useRequestTypeId,
|
||||
APPROVAL_PHASE
|
||||
APPROVAL_PHASE,
|
||||
isApprovedStatus
|
||||
} from '../../shared/approval';
|
||||
import { navigate } from '../../shared/routing';
|
||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||
@@ -169,7 +170,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
<Icon name="copy" size="sm" />
|
||||
Организация рабочего места на основе этой
|
||||
</Button>
|
||||
<PrintButton />
|
||||
{isApprovedStatus(status) && <PrintButton />}
|
||||
</div>
|
||||
|
||||
<div className="rp-print-root">
|
||||
|
||||
@@ -18,7 +18,7 @@ function parseJsonArray<T>(raw: unknown): T[] {
|
||||
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||
'Phone', 'HireType', 'ReplacedEmployee', 'Project', 'ProjectId', 'ProjectCode',
|
||||
'CandidateName', 'HireDate', 'Position', 'WorkKind',
|
||||
'SiteName', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod',
|
||||
'Department', 'DepartmentId', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod',
|
||||
'SalaryProbation', 'SalaryAfterProbation', 'WorkSchedule', 'WorkScheduleOther',
|
||||
'AgencyName', 'AgencyTerms',
|
||||
'SelectionRequestId', 'SelectionRequestListId', 'SelectionRequestCode'
|
||||
|
||||
@@ -34,7 +34,16 @@ export interface IFormData {
|
||||
HireDate: string;
|
||||
Position: string;
|
||||
WorkKind: string;
|
||||
SiteName: string;
|
||||
/**
|
||||
* Подразделение по справочнику (Dictionary/Departments, см.
|
||||
* shared/dictionary/DepartmentPicker) — как в WorkplaceSetup/RecruitingSelection:
|
||||
* ручной выбор, чисто информационное, на маршрут согласования НЕ влияет.
|
||||
* Заменило прежнее свободнотекстовое поле «Отдел / участок» (колонка SiteName
|
||||
* в SP осталась в схеме нетронутой — просто больше не используется формой).
|
||||
*/
|
||||
Department: string;
|
||||
/** Id отдела из справочника; '' — если введён вручную без выбора из списка. */
|
||||
DepartmentId: string;
|
||||
SiteLocation: string;
|
||||
// СИЗ
|
||||
PpeHeight: string;
|
||||
@@ -77,7 +86,8 @@ export const EMPTY_DATA: IFormData = {
|
||||
HireDate: '',
|
||||
Position: '',
|
||||
WorkKind: '',
|
||||
SiteName: '',
|
||||
Department: '',
|
||||
DepartmentId: '',
|
||||
SiteLocation: '',
|
||||
PpeHeight: '',
|
||||
PpeClothingSize: '',
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
{ "internalName": "HireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true },
|
||||
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
|
||||
{ "internalName": "WorkKind", "title": "Вид работ", "type": "Text" },
|
||||
{ "internalName": "SiteName", "title": "Отдел / участок", "type": "Text" },
|
||||
{ "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" },
|
||||
{ "internalName": "Department", "title": "Подразделение (справочник)", "type": "Text" },
|
||||
{ "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" },
|
||||
{ "internalName": "SiteLocation", "title": "Местоположение", "type": "Text" },
|
||||
|
||||
{ "internalName": "PpeHeight", "title": "СИЗ: рост", "type": "Number" },
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextInput } from '../../../shared/ui';
|
||||
import { DepartmentPicker } from '../../../shared/dictionary';
|
||||
import { ISectionProps } from '../model';
|
||||
|
||||
export const CandidateSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
export const CandidateSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||
<CollapseSection title="Кандидат" icon="people">
|
||||
<FormField label="Ф.И.О. кандидата" htmlFor="CandidateName" required full>
|
||||
<TextInput id="CandidateName" value={data.CandidateName} onChange={(v) => set('CandidateName', v)} />
|
||||
@@ -16,8 +17,16 @@ export const CandidateSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
<FormField label="Вид работ" htmlFor="WorkKind">
|
||||
<TextInput id="WorkKind" value={data.WorkKind} onChange={(v) => set('WorkKind', v)} />
|
||||
</FormField>
|
||||
<FormField label="Отдел / участок" htmlFor="SiteName">
|
||||
<TextInput id="SiteName" value={data.SiteName} onChange={(v) => set('SiteName', v)} />
|
||||
<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="SiteLocation">
|
||||
<TextInput id="SiteLocation" value={data.SiteLocation} onChange={(v) => set('SiteLocation', v)} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -102,6 +102,26 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
};
|
||||
}, [editId, context]);
|
||||
|
||||
// Выбор другого заявителя подтягивает его ФИО (для печати) и организации (как
|
||||
// в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь
|
||||
// дополнительно берём ФИО, т.к. TMC хранит его отдельным полем AuthorName).
|
||||
const handleRequesterChange = (login: string): void => {
|
||||
setData((prev) => ({ ...prev, RequesterEmail: login }));
|
||||
if (!login) {
|
||||
setData((prev) => ({ ...prev, Organization: '' }));
|
||||
return;
|
||||
}
|
||||
getEmployeeOrganizations(context, login)
|
||||
.then((records) =>
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
AuthorName: records[0]?.fio || prev.AuthorName,
|
||||
Organization: formatEmployeeOrganizations(records)
|
||||
}))
|
||||
)
|
||||
.catch(() => undefined);
|
||||
};
|
||||
|
||||
// Открыта СВОЯ заявка на редактирование — подгружаем содержимое, статус и вложения.
|
||||
useEffect(() => {
|
||||
if (!editId) {
|
||||
@@ -131,7 +151,9 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
// не нашлось в реестре, форма сюда вообще не смонтировалась бы, поэтому здесь
|
||||
// tmcTransferApprovalService можно звать не опасаясь исключения.
|
||||
const approval = useMemo(() => tmcTransferApprovalService(editListId), [editListId]);
|
||||
const userEmail = effectiveUserEmail(context);
|
||||
// Маршрут резолвится по e-mail ВЫБРАННОГО заявителя (не обязательно текущего
|
||||
// пользователя) — так работает подача за другого, как в Vacation.
|
||||
const userEmail = data.RequesterEmail;
|
||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||
@@ -142,13 +164,13 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
// «руководитель проекта-источника». Меняется проект → перепланируем маршрут.
|
||||
const projectId = data.SourceProjectId || null;
|
||||
const targetProjectId = data.TargetProjectId || null;
|
||||
// Маршрут согласования зависит от проекта-источника («Откуда»): пока он не
|
||||
// выбран, resolve вернёт 404 — не грузим, показываем заглушку. У открытой на
|
||||
// редактирование заявки маршрут уже записан, источник не нужен.
|
||||
const matrixReady = !!editId || (!!projectId && !!requestTypeId);
|
||||
// Маршрут согласования зависит от заявителя и проекта-источника («Откуда»):
|
||||
// пока не выбраны, resolve вернёт 404 — не грузим, показываем заглушку. У
|
||||
// открытой на редактирование заявки маршрут уже записан, ни то ни другое не нужно.
|
||||
const matrixReady = !!editId || (!!userEmail && !!projectId && !!requestTypeId);
|
||||
// Исполнитель определяется проектом-получателем («Куда»): показываем блок с
|
||||
// заглушкой, пока «Куда» не выбрано. Источник тоже нужен, иначе resolve 404.
|
||||
const executorsReady = !!editId || (!!projectId && !!targetProjectId && !!requestTypeId);
|
||||
// заглушкой, пока «Куда» не выбрано. Источник и заявитель тоже нужны, иначе resolve 404.
|
||||
const executorsReady = !!editId || (!!userEmail && !!projectId && !!targetProjectId && !!requestTypeId);
|
||||
const loadMatrix = useCallback(
|
||||
() =>
|
||||
editId
|
||||
@@ -182,10 +204,13 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
const hasItems = data.Items.some((i) => i.name.trim().length > 0);
|
||||
const isValid =
|
||||
data.SourceProject.trim().length > 0 && data.TargetProject.trim().length > 0 && hasItems;
|
||||
!!data.RequesterEmail &&
|
||||
data.SourceProject.trim().length > 0 &&
|
||||
data.TargetProject.trim().length > 0 &&
|
||||
hasItems;
|
||||
const validate = (): string | undefined => {
|
||||
if (!isValid) {
|
||||
return 'Заполните обязательные поля: проект-источник, проект-получатель и хотя бы одну позицию ТМЦ.';
|
||||
return 'Заполните обязательные поля: заявитель, проект-источник, проект-получатель и хотя бы одну позицию ТМЦ.';
|
||||
}
|
||||
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
||||
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
|
||||
@@ -278,7 +303,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
const handleSubmit = (): Promise<void> =>
|
||||
submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
||||
|
||||
const sectionProps = { data, set, context };
|
||||
const sectionProps = { data, set, context, onRequesterChange: handleRequesterChange };
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -294,6 +319,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
<InitiatorLine
|
||||
listTitle={editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID)}
|
||||
itemId={requestId}
|
||||
fallback={{ name: data.AuthorName, email: data.RequesterEmail }}
|
||||
/>
|
||||
}
|
||||
headerRight={
|
||||
@@ -334,7 +360,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
editor={matrix}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
waiting={!matrixReady}
|
||||
waitingText="Выберите проект-источник («Откуда»), чтобы построить маршрут согласования."
|
||||
waitingText="Укажите заявителя и проект-источник («Откуда»), чтобы построить маршрут согласования."
|
||||
/>
|
||||
|
||||
{/* Блок исполнителя показываем, только когда по маршруту реально есть
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: $sp-3;
|
||||
margin-bottom: $sp-4;
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
FormField,
|
||||
AttachmentList,
|
||||
PrintButton,
|
||||
PrintOrgSelect,
|
||||
OrgLetterhead,
|
||||
Icon
|
||||
} from '../../shared/ui';
|
||||
import { navigate } from '../../shared/routing';
|
||||
@@ -57,6 +59,9 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
||||
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Какая из организаций заявителя (см. OrganizationField) идёт в печать —
|
||||
// выбор в тулбаре через PrintOrgSelect, по умолчанию первая.
|
||||
const [printOrgIndex, setPrintOrgIndex] = useState<number>(0);
|
||||
|
||||
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
||||
// В просмотре форма read-only, но секции ждут set — даём no-op.
|
||||
@@ -128,7 +133,7 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
||||
);
|
||||
}
|
||||
|
||||
const sectionProps = { data, set, context };
|
||||
const sectionProps = { data, set, context, printOrgIndex };
|
||||
|
||||
const notify = (): void => {
|
||||
if (!requestTypeId) return;
|
||||
@@ -151,10 +156,12 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
||||
Редактировать
|
||||
</Button>
|
||||
)}
|
||||
<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}`}
|
||||
|
||||
@@ -39,6 +39,8 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
||||
'RequesterEmail',
|
||||
'AuthorName',
|
||||
'Organization',
|
||||
'Department',
|
||||
'DepartmentId',
|
||||
'Subject',
|
||||
'RequestDate',
|
||||
'SourceProject',
|
||||
@@ -62,6 +64,8 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
||||
RequesterEmail: (item.RequesterEmail as string) || '',
|
||||
AuthorName: (item.AuthorName as string) || '',
|
||||
Organization: (item.Organization as string) || '',
|
||||
Department: (item.Department as string) || '',
|
||||
DepartmentId: (item.DepartmentId as string) || '',
|
||||
Subject: (item.Subject as string) || '',
|
||||
RequestDate: requestDate.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
SourceProject: (item.SourceProject as string) || '',
|
||||
|
||||
@@ -18,14 +18,25 @@ export interface IFormData {
|
||||
// Шапка служебной записки
|
||||
Recipient: string; // Кому (по умолч. «Директору Самаевой А.А.»)
|
||||
// NB: не «Author» — это зарезервированное системное имя колонки SP (Created By).
|
||||
RequesterEmail: string; // От — email заявителя (текущий пользователь)
|
||||
AuthorName: string; // От — Ф.И.О. заявителя (для печати)
|
||||
// Выбирается через PeopleField (по умолчанию — текущий пользователь, но можно
|
||||
// подать за другого — см. handleRequesterChange в TmcTransferForm.tsx). Именно
|
||||
// это поле идёт в getPlannedMatrix: маршрут резолвится на ВЫБРАННОГО заявителя.
|
||||
RequesterEmail: string; // От — email заявителя
|
||||
AuthorName: string; // От — Ф.И.О. заявителя (для печати; подтягивается из Employee/ByEmail при выборе)
|
||||
/**
|
||||
* Организации заявителя (`\n`-joined «Компания — должность» на строку) —
|
||||
* подтягивается автоматически из Employee/ByEmail, read-only (см.
|
||||
* shared/ui/OrganizationField).
|
||||
*/
|
||||
Organization: string;
|
||||
/**
|
||||
* Подразделение (справочник Dictionary/Departments, см.
|
||||
* shared/dictionary/DepartmentPicker) — необязательное, справочное, на
|
||||
* маршрут согласования НЕ влияет.
|
||||
*/
|
||||
Department: string;
|
||||
/** Id отдела из справочника; '' — если введён вручную без выбора из списка. */
|
||||
DepartmentId: string;
|
||||
Subject: string; // Тема (по умолч. «Перемещение ТМЦ»)
|
||||
RequestDate: string; // Дата (YYYY-MM-DD)
|
||||
|
||||
@@ -55,16 +66,27 @@ export interface ISectionProps {
|
||||
data: IFormData;
|
||||
set: SetField;
|
||||
context: WebPartContext;
|
||||
/** Индекс организации для печати (см. shared/ui/PrintOrgSelect и OrganizationField.printIndex). */
|
||||
printOrgIndex?: number;
|
||||
/**
|
||||
* Выбор заявителя подтягивает его ФИО и организации — поэтому у поля отдельный
|
||||
* обработчик. Если не передан (режим чтения) — правка через set.
|
||||
*/
|
||||
onRequesterChange?: (login: string) => void;
|
||||
}
|
||||
|
||||
export const DEFAULT_RECIPIENT = 'Директору Самаевой А.А.';
|
||||
export const DEFAULT_SUBJECT = 'Перемещение ТМЦ';
|
||||
/** Плейсхолдер для поля выбора заявителя. */
|
||||
export const REQUESTER_PLACEHOLDER = 'Начните вводить Ф.И.О. заявителя…';
|
||||
|
||||
export const EMPTY_DATA: IFormData = {
|
||||
Recipient: DEFAULT_RECIPIENT,
|
||||
RequesterEmail: '',
|
||||
AuthorName: '',
|
||||
Organization: '',
|
||||
Department: '',
|
||||
DepartmentId: '',
|
||||
Subject: DEFAULT_SUBJECT,
|
||||
RequestDate: '',
|
||||
SourceProject: '',
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
{ "internalName": "RequesterEmail", "title": "От (email заявителя)", "type": "Text" },
|
||||
{ "internalName": "AuthorName", "title": "От (Ф.И.О.)", "type": "Text" },
|
||||
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
|
||||
{ "internalName": "Department", "title": "Подразделение", "type": "Text" },
|
||||
{ "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" },
|
||||
{ "internalName": "Subject", "title": "Тема", "type": "Text" },
|
||||
{ "internalName": "RequestDate", "title": "Дата", "type": "DateTime", "dateOnly": true },
|
||||
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { FormGrid, FormField, TextInput, TextArea, OrganizationField } from '../../../shared/ui';
|
||||
import { ProjectPicker } from '../../../shared/dictionary';
|
||||
import { ISectionProps } from '../model';
|
||||
import { FormGrid, FormField, TextInput, TextArea, PeopleField, OrganizationField } from '../../../shared/ui';
|
||||
import { DepartmentPicker, ProjectPicker } from '../../../shared/dictionary';
|
||||
import { ISectionProps, REQUESTER_PLACEHOLDER } from '../model';
|
||||
|
||||
/**
|
||||
* Шапка служебной записки: заявитель, проекты «откуда → куда», дата, обоснование.
|
||||
* «Кому» и «Тема» — константы служебки (пишутся из модели по умолчанию), в форме не показываются.
|
||||
*/
|
||||
export const HeaderSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||
export const HeaderSection: React.FC<ISectionProps> = ({ data, set, context, printOrgIndex, onRequesterChange }) => (
|
||||
<FormGrid>
|
||||
<OrganizationField value={data.Organization} />
|
||||
<FormField label="Заявитель" htmlFor="RequesterEmail" required full>
|
||||
<PeopleField
|
||||
context={context}
|
||||
value={data.RequesterEmail}
|
||||
onChange={onRequesterChange ?? ((v) => set('RequesterEmail', v))}
|
||||
placeholder={REQUESTER_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
<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="SourceProject" required>
|
||||
<ProjectPicker
|
||||
id="SourceProject"
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 './VacationView.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 VacationView: React.FC<IVacationViewProps> = ({ 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 (не смонтировались бы сюда иначе) —
|
||||
// vacationApprovalService можно звать не опасаясь исключения.
|
||||
@@ -98,7 +101,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
||||
);
|
||||
}
|
||||
|
||||
const sectionProps = { data, set: NOOP_SET, context };
|
||||
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
|
||||
|
||||
const notify = (): void => {
|
||||
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
|
||||
@@ -115,10 +118,12 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
||||
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}`}
|
||||
|
||||
@@ -29,6 +29,10 @@ export async function loadVacationRequest(itemId: number, listId?: string): Prom
|
||||
'StartDate',
|
||||
'DaysCount',
|
||||
'Organization',
|
||||
'Department',
|
||||
'DepartmentId',
|
||||
'Project',
|
||||
'ProjectId',
|
||||
'RequestStatus',
|
||||
'RequestUniqueCode',
|
||||
'Employee/Title',
|
||||
@@ -49,7 +53,11 @@ export async function loadVacationRequest(itemId: number, listId?: string): Prom
|
||||
StartDate: start.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
DaysCount: days === null || days === undefined ? '' : String(days),
|
||||
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 {
|
||||
|
||||
@@ -32,6 +32,18 @@ export interface IFormData {
|
||||
* сразу в нескольких организациях — тогда строк несколько.
|
||||
*/
|
||||
Organization: string;
|
||||
/**
|
||||
* Подразделение (справочник Dictionary/Departments, см.
|
||||
* shared/dictionary/DepartmentPicker) — необязательное, справочное, на
|
||||
* маршрут согласования НЕ влияет.
|
||||
*/
|
||||
Department: string;
|
||||
/** Id отдела из справочника; '' — если введён вручную без выбора из списка. */
|
||||
DepartmentId: string;
|
||||
/** Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — необязательное, справочное. */
|
||||
Project: string;
|
||||
/** oid проекта ('' — если введён вручную без выбора из списка). */
|
||||
ProjectId: string;
|
||||
}
|
||||
|
||||
/** Обновление одного поля формы с сохранением типа значения. */
|
||||
@@ -47,6 +59,8 @@ export interface ISectionProps {
|
||||
* отдельный обработчик. Если не передан (режим чтения) — правка через set.
|
||||
*/
|
||||
onEmployeeChange?: (login: string) => void;
|
||||
/** Индекс организации для печати (см. shared/ui/PrintOrgSelect и OrganizationField.printIndex). */
|
||||
printOrgIndex?: number;
|
||||
}
|
||||
|
||||
export const EMPTY_DATA: IFormData = {
|
||||
@@ -55,7 +69,11 @@ export const EMPTY_DATA: IFormData = {
|
||||
StartDate: '',
|
||||
DaysCount: '',
|
||||
Substitute: '',
|
||||
Organization: ''
|
||||
Organization: '',
|
||||
Department: '',
|
||||
DepartmentId: '',
|
||||
Project: '',
|
||||
ProjectId: ''
|
||||
};
|
||||
|
||||
/** Плейсхолдер для поля выбора сотрудника. */
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
{ "internalName": "EndDate", "title": "Дата окончания", "type": "DateTime", "dateOnly": true },
|
||||
{ "internalName": "DaysCount", "title": "Количество дней", "type": "Number" },
|
||||
{ "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, 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"
|
||||
|
||||
@@ -47,7 +47,11 @@ function buildItem(
|
||||
DaysCount: Number.isFinite(days) && days > 0 ? days : null,
|
||||
EndDate: computeEndDate(data.StartDate, days) || 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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: $sp-3;
|
||||
margin-bottom: $sp-4;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
FormField,
|
||||
AttachmentList,
|
||||
PrintButton,
|
||||
PrintOrgSelect,
|
||||
OrgLetterhead,
|
||||
Icon
|
||||
} from '../../shared/ui';
|
||||
import { navigate } from '../../shared/routing';
|
||||
@@ -63,6 +65,9 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [canEditFields, setCanEditFields] = useState<boolean>(false);
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||
// Какая из организаций заявителя (см. OrganizationField) идёт в печать —
|
||||
// выбор в тулбаре через PrintOrgSelect, по умолчанию первая.
|
||||
const [printOrgIndex, setPrintOrgIndex] = useState<number>(0);
|
||||
|
||||
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||
@@ -182,7 +187,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
);
|
||||
}
|
||||
|
||||
const sectionProps = { data, set, context };
|
||||
const sectionProps = { data, set, context, printOrgIndex };
|
||||
|
||||
const notify = (): void => {
|
||||
if (!requestTypeId) return;
|
||||
@@ -205,6 +210,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
Редактировать
|
||||
</Button>
|
||||
)}
|
||||
<PrintOrgSelect value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
||||
<PrintButton />
|
||||
</div>
|
||||
|
||||
@@ -222,6 +228,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
)}
|
||||
|
||||
<div className="rp-print-root">
|
||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||
<FormCard
|
||||
title="Организация рабочего места"
|
||||
subtitle={`№${code || itemId}`}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { dataListTitle } from '../../shared/listTitles';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||
'FullName', 'NameLatin', 'Phone', 'Position', 'Department', 'DepartmentId', 'WorkKind', 'Location', 'StartDate',
|
||||
'FullName', 'NameLatin', 'Phone', 'Position', 'Department', 'DepartmentId', 'Project', 'ProjectId', 'WorkKind', 'Location', 'StartDate',
|
||||
'Software', 'AccessRights', 'Organization'
|
||||
];
|
||||
|
||||
|
||||
@@ -21,6 +21,14 @@ export interface IFormData {
|
||||
Department: string;
|
||||
/** Id отдела из справочника (Dictionary/Departments); '' — если введён вручную без выбора из списка. */
|
||||
DepartmentId: string;
|
||||
/**
|
||||
* Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — отдельно
|
||||
* от Department: то влияет на маршрут согласования (getPlannedMatrix в
|
||||
* WorkplaceSetupForm.tsx), это поле чисто информативное.
|
||||
*/
|
||||
Project: string;
|
||||
/** oid проекта ('' — если введён вручную без выбора из списка). */
|
||||
ProjectId: string;
|
||||
WorkKind: string;
|
||||
Location: string;
|
||||
StartDate: string;
|
||||
@@ -45,6 +53,8 @@ export interface ISectionProps {
|
||||
data: IFormData;
|
||||
set: SetField;
|
||||
context: WebPartContext;
|
||||
/** Индекс организации для печати (см. shared/ui/PrintOrgSelect и OrganizationField.printIndex). */
|
||||
printOrgIndex?: number;
|
||||
}
|
||||
|
||||
export const EMPTY_DATA: IFormData = {
|
||||
@@ -54,6 +64,8 @@ export const EMPTY_DATA: IFormData = {
|
||||
Position: '',
|
||||
Department: '',
|
||||
DepartmentId: '',
|
||||
Project: '',
|
||||
ProjectId: '',
|
||||
WorkKind: '',
|
||||
Location: '',
|
||||
StartDate: '',
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function loadPrefillFromHire(hireId: number): Promise<IFormData> {
|
||||
FullName: hire.CandidateName,
|
||||
Phone: hire.Phone,
|
||||
Position: hire.Position,
|
||||
Department: hire.SiteName,
|
||||
Department: hire.Department,
|
||||
WorkKind: hire.WorkKind,
|
||||
Location: hire.SiteLocation,
|
||||
StartDate: hire.HireDate
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
{ "internalName": "NameLatin", "title": "Фамилия и имя (латиница)", "type": "Text" },
|
||||
{ "internalName": "Phone", "title": "Телефон", "type": "Text" },
|
||||
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
|
||||
{ "internalName": "Department", "title": "Отдел / участок", "type": "Text" },
|
||||
{ "internalName": "Department", "title": "Подразделение", "type": "Text" },
|
||||
{ "internalName": "DepartmentId", "title": "Id отдела (справочник)", "type": "Text" },
|
||||
{ "internalName": "Project", "title": "Проект", "type": "Text" },
|
||||
{ "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" },
|
||||
{ "internalName": "WorkKind", "title": "Вид работ", "type": "Text" },
|
||||
{ "internalName": "Location", "title": "Местоположение", "type": "Text" },
|
||||
{ "internalName": "StartDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextInput, PhoneInput, OrganizationField } from '../../../shared/ui';
|
||||
import { DepartmentPicker } from '../../../shared/dictionary';
|
||||
import { DepartmentPicker, ProjectPicker } from '../../../shared/dictionary';
|
||||
import { ISectionProps } from '../model';
|
||||
|
||||
export const EmployeeSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||
export const EmployeeSection: React.FC<ISectionProps> = ({ data, set, context, printOrgIndex }) => (
|
||||
<CollapseSection title="Сотрудник" icon="people">
|
||||
<FormField label="Ф.И.О." htmlFor="FullName" required full>
|
||||
<TextInput id="FullName" value={data.FullName} onChange={(v) => set('FullName', v)} />
|
||||
@@ -17,7 +17,7 @@ export const EmployeeSection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
<FormField label="Должность" htmlFor="Position" required>
|
||||
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
|
||||
</FormField>
|
||||
<FormField label="Отдел / участок" htmlFor="Department">
|
||||
<FormField label="Подразделение" htmlFor="Department">
|
||||
<DepartmentPicker
|
||||
id="Department"
|
||||
context={context}
|
||||
@@ -28,6 +28,18 @@ export const EmployeeSection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
}}
|
||||
/>
|
||||
</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="WorkKind">
|
||||
<TextInput id="WorkKind" value={data.WorkKind} onChange={(v) => set('WorkKind', v)} />
|
||||
</FormField>
|
||||
@@ -37,6 +49,6 @@ export const EmployeeSection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
<FormField label="Дата приёма" htmlFor="StartDate">
|
||||
<TextInput id="StartDate" type="date" value={data.StartDate} onChange={(v) => set('StartDate', v)} />
|
||||
</FormField>
|
||||
<OrganizationField value={data.Organization} label="Организация заявителя" />
|
||||
<OrganizationField value={data.Organization} label="Организация заявителя" printIndex={printOrgIndex} />
|
||||
</CollapseSection>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ export {
|
||||
isEditableStatus,
|
||||
initialSubmitStatus,
|
||||
isSubmittedStatus,
|
||||
isApprovedStatus,
|
||||
} from './models/statuses';
|
||||
export type { ISiteUser } from './models/ISiteUser';
|
||||
export type { IMatrixRow } from './models/IMatrixRow';
|
||||
|
||||
@@ -82,6 +82,23 @@ export function isSubmittedStatus(status: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Заявка прошла согласование (кнопка «Печать» открывается только тут).
|
||||
*
|
||||
* Формы без этапа исполнения (напр. «Заявка на подбор») получают финальный
|
||||
* статус «Согласовано» сразу. Формы с исполнителями (Hire, Referral) минуют
|
||||
* «Согласовано» — из computeRequestStatus в approverDecision.service.ts статус
|
||||
* сразу становится «Выполнение», а по завершении — «Закрыта». Поэтому здесь
|
||||
* нужны все три статуса, иначе печать для таких форм не появится никогда.
|
||||
*/
|
||||
export function isApprovedStatus(status: string): boolean {
|
||||
return (
|
||||
status === REQUEST_STATUS.approved ||
|
||||
status === REQUEST_STATUS.execution ||
|
||||
status === REQUEST_STATUS.closed
|
||||
);
|
||||
}
|
||||
|
||||
// Наборов значений ДВА, и они не совпадают — это разные сущности:
|
||||
//
|
||||
// 1) статус ЗАЯВКИ — колонка RequestStatus в списке «… — данные». Пишется
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Позволяет TypeScript импортировать растровые/векторные изображения как модуль,
|
||||
// отдающий URL строкой (см. shared/orgLogos.ts) — саму подстановку URL при сборке
|
||||
// делает webpack (см. config/typescript.json → staticAssetsToCopy), тайпчекеру
|
||||
// нужна только сигнатура.
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import tansuConstruction from '../assets/logos/tansu-construction.png';
|
||||
import astelK from '../assets/logos/astel-k.png';
|
||||
import ctGlobal from '../assets/logos/ctglobal.png';
|
||||
import kazpromService from '../assets/logos/kazprom-service.png';
|
||||
import qazgeomax from '../assets/logos/qazgeomax.png';
|
||||
|
||||
/**
|
||||
* Логотипы организаций для брендированной шапки печати (см. shared/ui/OrgLetterhead).
|
||||
* Ключ — ТОЧНОЕ название компании, как отдаёт GET /Dictionary/Companies (см.
|
||||
* shared/dictionary/companies.service.ts) и GET /Employee/ByEmail (IEmployeeOrganization.company) —
|
||||
* оба идут из одного источника (кадровый API), названия совпадают дословно.
|
||||
*
|
||||
* Не для каждой компании из справочника есть логотип — тогда OrgLetterhead просто
|
||||
* не рендерится (lookup возвращает undefined). Новая компания → положить PNG в
|
||||
* ../assets/logos и добавить сюда запись.
|
||||
*/
|
||||
export const ORG_LOGOS: Record<string, string> = {
|
||||
'ТОО TANSU Construction': tansuConstruction,
|
||||
'ТОО АСТЕЛ-К': astelK,
|
||||
'ТОО СТ Global': ctGlobal,
|
||||
'ТОО KazPromService': kazpromService,
|
||||
'ТОО QAZGEOMAX': qazgeomax
|
||||
};
|
||||
@@ -5,15 +5,22 @@ import { getCreatedBy, IInitiator } from '../../createdBy';
|
||||
export interface IInitiatorLineProps {
|
||||
/** Заголовок списка данных заявки. */
|
||||
listTitle: string;
|
||||
/** Id элемента заявки. undefined — заявка ещё не сохранена (ничего не показываем). */
|
||||
/** Id элемента заявки. undefined — заявка ещё не сохранена (см. fallback). */
|
||||
itemId?: number;
|
||||
/**
|
||||
* Показать вместо Created-By-lookup, пока заявка ещё не сохранена (itemId
|
||||
* не задан) — напр. в формах без поля «Сотрудник», где заявитель нигде
|
||||
* больше не виден до первого сохранения (см. TmcTransferForm.tsx).
|
||||
*/
|
||||
fallback?: { name?: string; email?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Строка «Инициатор: Ф.И.О. · email» под номером заявки. Сама подгружает
|
||||
* создателя (Created By) по listTitle+itemId. Пока не сохранено / нет данных — не рендерится.
|
||||
* создателя (Created By) по listTitle+itemId. Пока не сохранено — либо не
|
||||
* рендерится, либо (если передан fallback) показывает его вместо lookup'а.
|
||||
*/
|
||||
export const InitiatorLine: React.FC<IInitiatorLineProps> = ({ listTitle, itemId }) => {
|
||||
export const InitiatorLine: React.FC<IInitiatorLineProps> = ({ listTitle, itemId, fallback }) => {
|
||||
const [initiator, setInitiator] = React.useState<IInitiator | undefined>(undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -32,18 +39,20 @@ export const InitiatorLine: React.FC<IInitiatorLineProps> = ({ listTitle, itemId
|
||||
};
|
||||
}, [listTitle, itemId]);
|
||||
|
||||
if (!initiator || (!initiator.name && !initiator.email)) {
|
||||
const shown = itemId ? initiator : fallback;
|
||||
|
||||
if (!shown || (!shown.name && !shown.email)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.line}>
|
||||
<span className={styles.label}>Инициатор:</span>
|
||||
{initiator.name && <span className={styles.name}>{initiator.name}</span>}
|
||||
{initiator.name && initiator.email && <span className={styles.sep}>·</span>}
|
||||
{initiator.email && (
|
||||
<a className={styles.email} href={`mailto:${initiator.email}`}>
|
||||
{initiator.email}
|
||||
{shown.name && <span className={styles.name}>{shown.name}</span>}
|
||||
{shown.name && shown.email && <span className={styles.sep}>·</span>}
|
||||
{shown.email && (
|
||||
<a className={styles.email} href={`mailto:${shown.email}`}>
|
||||
{shown.email}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@import '../../../../../styles/design-tokens';
|
||||
|
||||
.letterhead {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding-left: $sp-3;
|
||||
margin-bottom: $sp-4;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 96px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
import styles from './OrgLetterhead.module.scss';
|
||||
import { ORG_LOGOS } from '../../orgLogos';
|
||||
|
||||
interface IOrgLetterheadProps {
|
||||
/** `\n`-joined «Компания — должность», как в data.Organization. */
|
||||
value: string;
|
||||
selectedIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Брендированная плашка над FormCard — логотип организации, выбранной в
|
||||
* PrintOrgSelect (видна и на экране, и в печати). Название компании — часть
|
||||
* строки до « — должность» (см. formatEmployeeOrganizations). Если логотипа
|
||||
* для этой компании нет (shared/orgLogos.ts) — ничего не рендерим.
|
||||
*/
|
||||
export const OrgLetterhead: React.FC<IOrgLetterheadProps> = ({ value, selectedIndex }) => {
|
||||
const lines = value ? value.split('\n') : [];
|
||||
const line = lines[selectedIndex];
|
||||
if (!line) return null;
|
||||
|
||||
const company = line.split(' — ')[0].trim();
|
||||
const logo = ORG_LOGOS[company];
|
||||
if (!logo) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.letterhead}>
|
||||
<img src={logo} alt={company} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,14 @@ interface IOrganizationFieldProps {
|
||||
* читалось как «организация того, о ком заявка».
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* Индекс строки, которая остаётся в печатной версии (см. PrintOrgSelect в
|
||||
* тулбаре *View.tsx) — остальные строки помечаются `rp-no-print` и PrintButton
|
||||
* вырезает их из клона перед печатью (shared/ui/PrintButton живёт в
|
||||
* @tnsu/ui-kit). На экране видны ВСЕ строки независимо от выбора — это только
|
||||
* для печати. Не передан → печатаются все организации как раньше.
|
||||
*/
|
||||
printIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,15 +28,18 @@ interface IOrganizationFieldProps {
|
||||
* тогда строк несколько. Пока не резолвилось (сотрудник не выбран/не найден) —
|
||||
* ничего не рендерим, а не пустой блок.
|
||||
*/
|
||||
export const OrganizationField: React.FC<IOrganizationFieldProps> = ({ value, label = 'Организация' }) => {
|
||||
export const OrganizationField: React.FC<IOrganizationFieldProps> = ({ value, label = 'Организация', printIndex }) => {
|
||||
const lines = value ? value.split('\n') : [];
|
||||
if (lines.length === 0) return null;
|
||||
|
||||
return (
|
||||
<FormField label={label} full>
|
||||
<div className={styles.list}>
|
||||
{lines.map((line) => (
|
||||
<div key={line} className={styles.line}>
|
||||
{lines.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={printIndex !== undefined && i !== printIndex ? `${styles.line} rp-no-print` : styles.line}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@import '../../../../../styles/design-tokens';
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
font-family: $font-sans;
|
||||
font-size: $fs-sm;
|
||||
color: $gray-500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
import styles from './PrintOrgSelect.module.scss';
|
||||
import { Select, ISelectOption } from '@tnsu/ui-kit';
|
||||
|
||||
interface IPrintOrgSelectProps {
|
||||
/** `\n`-joined «Компания — должность», как в data.Organization. */
|
||||
value: string;
|
||||
selectedIndex: number;
|
||||
onChange: (index: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбор организации для печати — сотрудник может состоять сразу в нескольких
|
||||
* (см. shared/ui/OrganizationField), а печатный документ должен ссылаться на
|
||||
* одну конкретную. Живёт в тулбаре рядом с PrintButton, помечен `rp-no-print`
|
||||
* самим тулбаром — сам в печать не идёт. Рендерится, только если организаций
|
||||
* больше одной, иначе выбирать не из чего.
|
||||
*/
|
||||
export const PrintOrgSelect: React.FC<IPrintOrgSelectProps> = ({ value, selectedIndex, onChange }) => {
|
||||
const lines = value ? value.split('\n') : [];
|
||||
if (lines.length <= 1) return null;
|
||||
|
||||
const options: ISelectOption[] = lines.map((line, i) => ({ value: String(i), label: line }));
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<span className={styles.label}>Организация для печати</span>
|
||||
<Select value={String(selectedIndex)} onChange={(v) => onChange(Number(v))} options={options} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -39,6 +39,8 @@ export { parseExcelRows } from './ItemsTable/parseExcelRows';
|
||||
export { PeopleField } from './PeopleField/PeopleField';
|
||||
export { PeopleFieldMulti } from './PeopleField/PeopleFieldMulti';
|
||||
export { OrganizationField } from './OrganizationField/OrganizationField';
|
||||
export { PrintOrgSelect } from './PrintOrgSelect/PrintOrgSelect';
|
||||
export { OrgLetterhead } from './OrgLetterhead/OrgLetterhead';
|
||||
export { LinkField } from './LinkField/LinkField';
|
||||
export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay';
|
||||
export { SubmitActions } from './SubmitActions/SubmitActions';
|
||||
|
||||